Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🏗️ Build (push) Failing after 26s
Build & Deploy / 🧪 QA (push) Failing after 1m14s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
177 lines
5.1 KiB
TypeScript
177 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import { motion } from "framer-motion";
|
|
import { ArrowRight } from "lucide-react";
|
|
import Link from "next/link";
|
|
|
|
interface ButtonProps {
|
|
href: string;
|
|
children: React.ReactNode;
|
|
variant?: "primary" | "outline" | "ghost";
|
|
size?: "normal" | "large";
|
|
className?: string;
|
|
showArrow?: boolean;
|
|
onClick?: (e: React.MouseEvent<any>) => void;
|
|
[key: string]: any;
|
|
}
|
|
|
|
/**
|
|
* Premium Button: Pill-shaped, binary-accent hover effect, unified design.
|
|
*
|
|
* On hover:
|
|
* - A stream of binary characters scrolls across the button background
|
|
* - Primary: white binary on dark bg
|
|
* - Outline: blue binary on transparent bg
|
|
* - Subtle, fast, and satisfying
|
|
*/
|
|
export const Button: React.FC<ButtonProps> = ({
|
|
href,
|
|
children,
|
|
variant = "primary",
|
|
size = "normal",
|
|
className = "",
|
|
showArrow = true,
|
|
onClick,
|
|
...props
|
|
}) => {
|
|
const [hovered, setHovered] = React.useState(false);
|
|
const [displayText, setDisplayText] = React.useState<string | null>(null);
|
|
const contentRef = React.useRef<HTMLSpanElement>(null);
|
|
|
|
// Binary scramble on hover
|
|
React.useEffect(() => {
|
|
if (!hovered) {
|
|
setDisplayText(null);
|
|
return;
|
|
}
|
|
const original = contentRef.current?.textContent || "";
|
|
if (!original) return;
|
|
const chars = original.split("");
|
|
const total = 20;
|
|
let frame = 0;
|
|
const iv = setInterval(() => {
|
|
frame++;
|
|
const s = chars.map((c, i) => {
|
|
if (c === " ") return " ";
|
|
const settle = (frame / total) * chars.length;
|
|
if (i < settle) return c;
|
|
return Math.random() > 0.5 ? "1" : "0";
|
|
});
|
|
setDisplayText(s.join(""));
|
|
if (frame >= total) {
|
|
setDisplayText(null);
|
|
clearInterval(iv);
|
|
}
|
|
}, 1000 / 60);
|
|
return () => clearInterval(iv);
|
|
}, [hovered]);
|
|
|
|
const [binaryStr, setBinaryStr] = React.useState(
|
|
"0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1",
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
setBinaryStr(
|
|
Array.from({ length: 60 }, () => (Math.random() > 0.5 ? "0" : "1")).join(
|
|
" ",
|
|
),
|
|
);
|
|
}, []);
|
|
|
|
const base =
|
|
"relative inline-flex items-center justify-center gap-3 overflow-hidden rounded-full font-bold uppercase tracking-[0.15em] transition-all duration-300 group cursor-pointer";
|
|
|
|
const sizes: Record<string, string> = {
|
|
normal: "px-8 py-4 text-[10px]",
|
|
large: "px-10 py-5 text-[11px]",
|
|
};
|
|
|
|
const variants: Record<string, string> = {
|
|
primary:
|
|
"bg-slate-900 text-white hover:shadow-xl hover:shadow-slate-900/20 hover:-translate-y-0.5",
|
|
outline:
|
|
"border border-slate-200 bg-transparent text-slate-900 hover:border-slate-400 hover:-translate-y-0.5",
|
|
ghost: "bg-transparent text-slate-500 hover:text-slate-900",
|
|
};
|
|
|
|
// Binary stream overlay colors by variant
|
|
const binaryColor =
|
|
variant === "primary"
|
|
? "rgba(255,255,255,0.12)"
|
|
: variant === "outline"
|
|
? "rgba(59,130,246,0.15)"
|
|
: "rgba(148,163,184,0.15)";
|
|
|
|
const inner = (
|
|
<motion.span
|
|
onMouseEnter={() => setHovered(true)}
|
|
onMouseLeave={() => setHovered(false)}
|
|
className={`${base} ${sizes[size]} ${variants[variant]} ${className}`}
|
|
>
|
|
{/* Binary stream hover overlay */}
|
|
<span
|
|
className="absolute inset-0 flex items-center pointer-events-none overflow-hidden"
|
|
style={{ opacity: hovered ? 1 : 0, transition: "opacity 0.3s ease" }}
|
|
>
|
|
<motion.span
|
|
className="whitespace-nowrap font-mono text-xs tracking-[0.4em] select-none"
|
|
style={{ color: binaryColor }}
|
|
animate={hovered ? { x: [0, -200] } : { x: 0 }}
|
|
transition={
|
|
hovered ? { duration: 3, repeat: Infinity, ease: "linear" } : {}
|
|
}
|
|
>
|
|
{binaryStr} {binaryStr}
|
|
</motion.span>
|
|
</span>
|
|
|
|
{/* Shimmer line on hover (top edge) */}
|
|
<span
|
|
className="absolute top-0 left-0 right-0 h-px pointer-events-none"
|
|
style={{
|
|
opacity: hovered ? 1 : 0,
|
|
transition: "opacity 0.5s ease",
|
|
background:
|
|
variant === "primary"
|
|
? "linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent)"
|
|
: "linear-gradient(90deg, transparent, rgba(59,130,246,0.15), transparent)",
|
|
}}
|
|
/>
|
|
|
|
{/* Content */}
|
|
<span
|
|
className="relative z-10 flex items-center gap-3"
|
|
style={{ fontVariantNumeric: "tabular-nums" }}
|
|
>
|
|
<span ref={contentRef}>{displayText ?? children}</span>
|
|
{showArrow && (
|
|
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform duration-300" />
|
|
)}
|
|
</span>
|
|
</motion.span>
|
|
);
|
|
|
|
if (href.startsWith("#")) {
|
|
return (
|
|
<a
|
|
href={href}
|
|
onClick={(e) => {
|
|
if (onClick) onClick(e);
|
|
e.preventDefault();
|
|
document.querySelector(href)?.scrollIntoView({ behavior: "smooth" });
|
|
}}
|
|
{...props}
|
|
>
|
|
{inner}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Link href={href} onClick={onClick} {...props}>
|
|
{inner}
|
|
</Link>
|
|
);
|
|
};
|