Files
mintel.me/apps/web/src/components/Button.tsx
Marc Mintel 38f2cc8b85
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
feat(ui): enhance component share UX and add new interactive simulations
2026-02-22 16:58:42 +01:00

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}&nbsp;{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>
);
};