"use client"; import * as React from "react"; import { useInView } from "framer-motion"; /** * GlitchText: Binary scramble reveal effect * * Text characters scramble through random binary (0/1) before settling * into the final text. Creates a "decoding" feel. * * Can also be triggered on hover for buttons. */ interface GlitchTextProps { children: string; className?: string; delay?: number; duration?: number; trigger?: "inView" | "hover" | "mount"; as?: "span" | "div" | "h1" | "h2" | "p"; } const CHARS = "01"; export const GlitchText: React.FC = ({ children, className = "", delay = 0, duration = 0.8, trigger = "inView", as: Tag = "span", }) => { const text = children; const [display, setDisplay] = React.useState(text); const [isRevealed, setIsRevealed] = React.useState(false); const [isHovering, setIsHovering] = React.useState(false); const ref = React.useRef(null); const isInView = useInView(ref, { once: true, margin: "-10%" }); // Initial scramble reveal (on mount or in-view) React.useEffect(() => { if (isRevealed) return; const shouldStart = trigger === "mount" || (trigger === "inView" && isInView); if (!shouldStart) return; const chars = text.split(""); const totalFrames = Math.ceil(duration * 60); // ~60fps const staggerPerChar = totalFrames / chars.length; let frame = 0; const delayMs = delay * 1000; const timeout = setTimeout(() => { const interval = setInterval(() => { frame++; const revealed = chars.map((char, i) => { if (char === " ") return " "; const charFrame = i * staggerPerChar; if (frame > charFrame + staggerPerChar * 2) return char; // Settled if (frame > charFrame) { // Scrambling phase return CHARS[Math.floor(Math.random() * CHARS.length)]; } return CHARS[Math.floor(Math.random() * CHARS.length)]; }); setDisplay(revealed.join("")); if (frame >= totalFrames + staggerPerChar * 2) { setDisplay(text); setIsRevealed(true); clearInterval(interval); } }, 1000 / 60); return () => clearInterval(interval); }, delayMs); return () => clearTimeout(timeout); }, [isInView, trigger, text, delay, duration, isRevealed]); // Hover re-scramble (for buttons) React.useEffect(() => { if (!isHovering || trigger !== "hover") return; const chars = text.split(""); const totalFrames = 20; // Quick scramble let frame = 0; const interval = setInterval(() => { frame++; const scrambled = chars.map((char, i) => { if (char === " ") return " "; const settle = (frame / totalFrames) * chars.length; if (i < settle) return char; return CHARS[Math.floor(Math.random() * CHARS.length)]; }); setDisplay(scrambled.join("")); if (frame >= totalFrames) { setDisplay(text); clearInterval(interval); } }, 1000 / 60); return () => clearInterval(interval); }, [isHovering, trigger, text]); return ( setIsHovering(true) : undefined} onMouseLeave={ trigger === "hover" ? () => setIsHovering(false) : undefined } style={{ fontVariantNumeric: "tabular-nums" }} // Prevent layout shift during scramble > {display} ); };