{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"hyper-text","type":"registry:ui","title":"HyperText","description":"Text that scrambles into random uppercase letters and resolves character-by-character back to the original on hover, with a staggered per-letter reveal and an optional animate-on-load mode","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["text"],"registryDependencies":["https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/hyper-text/HyperText.tsx","content":"\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../utils\";\nimport \"./hyper-text.css\";\n\n/**\n * HyperText - Character scramble effect\n *\n * Displays text that scrambles through random characters on hover (or on load),\n * then resolves back to the original text. Each character is individually animated\n * with a staggered reveal.\n */\nexport interface HyperTextProps {\n\t/** Text to display and scramble */\n\ttext: string;\n\t/** Total animation duration in ms */\n\tduration?: number;\n\t/** Whether to animate on initial load */\n\tanimateOnLoad?: boolean;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Seed for the scramble-character stream. The same seed always produces the\n\t * same scramble sequence, which makes the effect reproducible in a test and\n\t * identical across mounts. The default is shared, so two `<HyperText />`\n\t * with no seed scramble alike.\n\t */\n\tseed?: number;\n}\n\nconst ALPHABETS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n\n/**\n * mulberry32 — a tiny deterministic PRNG, standing in for the source's bare\n * `Math.random()`. Seeding costs nothing visually (the same uniform\n * distribution) and buys a scramble that can be asserted on in a test.\n */\nfunction mulberry32(seed: number): () => number {\n\tlet state = seed >>> 0;\n\treturn () => {\n\t\tstate = (state + 0x6d2b79f5) >>> 0;\n\t\tlet t = state;\n\t\tt = Math.imul(t ^ (t >>> 15), t | 1);\n\t\tt ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n\t\treturn ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n\t};\n}\n\nexport function HyperText({\n\ttext,\n\tduration = 800,\n\tanimateOnLoad = false,\n\tclassName,\n\tseed = 1,\n}: HyperTextProps) {\n\tconst [displayText, setDisplayText] = useState<string[]>(() => text.split(\"\"));\n\t// Mirror of `displayText`, so a tick builds the next array before handing it to\n\t// the setter. React may call a state updater twice (StrictMode, in dev); an\n\t// updater that scrambled in place would draw twice from the seeded stream and\n\t// map the array twice per tick.\n\tconst displayTextRef = useRef(displayText);\n\tconst intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\tconst iterationsRef = useRef(0);\n\n\t// The Svelte interval handler reads `text` live; mirror that with a ref so a\n\t// running animation resolves toward the current prop, not a stale closure.\n\tconst textRef = useRef(text);\n\tconst seedRef = useRef(seed);\n\tuseEffect(() => {\n\t\ttextRef.current = text;\n\t\tseedRef.current = seed;\n\t});\n\n\t/** The single write path: keeps the ref mirror and the state in step. */\n\tfunction writeDisplayText(next: string[]) {\n\t\tdisplayTextRef.current = next;\n\t\tsetDisplayText(next);\n\t}\n\n\t// Sync displayText when text prop changes (the source's `$effect`).\n\tuseEffect(() => {\n\t\twriteDisplayText(text.split(\"\"));\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [text]);\n\n\tfunction stopAnimation() {\n\t\tif (intervalRef.current) {\n\t\t\tclearInterval(intervalRef.current);\n\t\t\tintervalRef.current = null;\n\t\t}\n\t}\n\n\tfunction startAnimation() {\n\t\tstopAnimation();\n\t\titerationsRef.current = 0;\n\t\tconst currentText = textRef.current;\n\t\tconst intervalMs = duration / (currentText.length * 10);\n\t\tconst random = mulberry32(seedRef.current);\n\t\tconst getRandomLetter = () => ALPHABETS.charAt(Math.floor(random() * ALPHABETS.length));\n\n\t\tintervalRef.current = setInterval(() => {\n\t\t\tconst target = textRef.current;\n\t\t\tif (iterationsRef.current < target.length) {\n\t\t\t\tconst next = displayTextRef.current.map((l, i) =>\n\t\t\t\t\tl === \" \" ? l : i <= iterationsRef.current ? (target[i] ?? l) : getRandomLetter()\n\t\t\t\t);\n\t\t\t\twriteDisplayText(next);\n\t\t\t\titerationsRef.current += 0.1;\n\t\t\t} else {\n\t\t\t\tstopAnimation();\n\t\t\t\twriteDisplayText(target.split(\"\"));\n\t\t\t}\n\t\t}, intervalMs);\n\t}\n\n\tfunction triggerAnimation() {\n\t\titerationsRef.current = 0;\n\t\tstartAnimation();\n\t}\n\n\t// onMount: optionally start, always clear the interval on unmount.\n\tuseEffect(() => {\n\t\tif (animateOnLoad) {\n\t\t\ttriggerAnimation();\n\t\t}\n\t\treturn stopAnimation;\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, []);\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\"hyper-text flex scale-100 cursor-default overflow-hidden py-2\", className)}\n\t\t\tonMouseEnter={triggerAnimation}\n\t\t\trole=\"presentation\"\n\t\t>\n\t\t\t<div className=\"flex\">\n\t\t\t\t{displayText.map((letter, i) => (\n\t\t\t\t\t<span\n\t\t\t\t\t\tkey={i}\n\t\t\t\t\t\tclassName={cn(\"hyper-text-char inline-block font-mono\", letter === \" \" ? \"w-3\" : \"\")}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tanimation: \"hyperFadeIn 0.3s ease forwards\",\n\t\t\t\t\t\t\tanimationDelay: `${i * (duration / (text.length * 10))}ms`,\n\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{letter.toUpperCase()}\n\t\t\t\t\t</span>\n\t\t\t\t))}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/hyper-text/HyperText.tsx"},{"path":"react/src/components/hyper-text/hyper-text.css","content":"/* Keyframes referenced by the inline `animation` shorthand on each\n   `.hyper-text-char`. The name is part of the visual contract — the Svelte\n   source declares it as a global keyframes block — so it is kept identical. */\n@keyframes hyperFadeIn {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateY(-10px);\n\t}\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/hyper-text/hyper-text.css"},{"path":"react/src/components/hyper-text/index.ts","content":"\"use client\";\n\nexport { HyperText } from \"./HyperText\";\nexport type { HyperTextProps } from \"./HyperText\";\n","type":"registry:ui","target":"@ui/fancy/components/hyper-text/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/hyper-text?framework=react"}}