"use client"; import * as React from "react"; import { useInViewOnce } from "./useInViewOnce"; const FRAME_MS = 32; const scramble = (text: string, revealed: number): string => Array.from(text) .map((char, index) => { if (index < revealed || !/[A-Za-z0-9]/.test(char)) return char; return Math.random() < 0.5 ? "0" : "1"; }) .join(""); /** * DecodeText: text that resolves from binary digits, left to right. Meant for * monospaced labels, where digits and letters have the same width and nothing shifts. * Screen readers and no-JS visitors get the plain text. */ export const DecodeText: React.FC<{ text: string; className?: string; /** Run when scrolled into view, on hover, or both. */ trigger?: "view" | "hover" | "both"; }> = ({ text, className = "", trigger = "view" }) => { const { ref, seen, still } = useInViewOnce(0.6); const [shown, setShown] = React.useState(text); const timer = React.useRef(0); const run = React.useCallback(() => { if (still) return; window.clearInterval(timer.current); const length = Array.from(text).length; const frames = Math.min(26, Math.max(12, length)); let frame = 0; timer.current = window.setInterval(() => { frame += 1; if (frame >= frames) { window.clearInterval(timer.current); setShown(text); return; } setShown(scramble(text, Math.floor((frame / frames) * length))); }, FRAME_MS); }, [still, text]); React.useEffect(() => { if (seen && trigger !== "hover") run(); return () => window.clearInterval(timer.current); }, [seen, trigger, run]); return ( {text} ); };