"use client"; import { useEffect, useState } from "react"; import { useReducedMotion } from "motion/react"; const GLYPHS = "!<>-_\\/[]{}=+*^?#ABCDEFX01"; type TextScrambleProps = { /** Words to cycle through. */ words: string[]; className?: string; /** Milliseconds each word stays. */ interval?: number; /** Characters shown while a letter is scrambling. */ glyphs?: string; }; /* TextScramble: letters shuffle through glyphs before settling, then the next word. */ export function TextScramble({ words, className = "", interval = 2600, glyphs = GLYPHS }: TextScrambleProps) { const reduce = useReducedMotion(); const [index, setIndex] = useState(0); const target = words[index % words.length] ?? ""; const [text, setText] = useState(target); useEffect(() => { if (words.length < 2) return; const id = window.setInterval(() => { if (!document.hidden) setIndex((i) => (i + 1) % words.length); }, interval); return () => window.clearInterval(id); }, [interval, words.length]); useEffect(() => { if (reduce) return; let tick = 0; let frame = 0; const step = () => { tick++; const out = target .split("") .map((ch, i) => (ch === " " || tick > i * 3 + 6 ? ch : glyphs[Math.floor(Math.random() * glyphs.length)])) .join(""); setText(out); if (out !== target) frame = requestAnimationFrame(step); }; frame = requestAnimationFrame(step); return () => cancelAnimationFrame(frame); }, [target, reduce, glyphs]); return ( {target} {reduce ? target : text} ); }