{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-texts-text-reveal",
  "title": "Text Reveal",
  "description": "Scramble-reveal animation that decodes characters progressively into the final text.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/components/texts/text-reveal/index.tsx",
      "content": "'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport { useInView } from 'motion/react';\n\nexport type CharacterPreset = 'default' | 'binary' | 'cyberpunk' | 'minimal';\n\nconst PRESETS: Record<CharacterPreset, string> = {\n  default: '#$%&@!?',\n  binary: '01',\n  cyberpunk: '░▒▓█◣◥',\n  minimal: '·+×÷',\n};\n\nexport interface TextRevealProps {\n  children: string;\n  speed?: number;\n  delay?: number;\n  className?: string;\n  triggerOnView?: boolean;\n  once?: boolean;\n  characters?: string;\n  preset?: CharacterPreset;\n}\n\nfunction pickRandom(chars: string, exclude?: string): string {\n  const pool = exclude ? [...chars].filter((c) => c !== exclude) : [...chars];\n  const source = pool.length > 0 ? pool : [...chars];\n  return source[Math.floor(Math.random() * source.length)];\n}\n\nexport function TextReveal({\n  children: text,\n  speed = 20,\n  delay = 0,\n  className = '',\n  triggerOnView = false,\n  once = true,\n  characters,\n  preset = 'default',\n}: TextRevealProps) {\n  const chars = characters ?? PRESETS[preset];\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once, margin: '-100px' });\n  const active = triggerOnView ? inView : true;\n\n  const [display, setDisplay] = useState('\\u00A0'.repeat(text.length));\n  const phaseRef = useRef<'scramble' | 'reveal'>('scramble');\n  const stepRef = useRef(0);\n  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n  const delayRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(() => {\n    if (!active) return;\n\n    const clearAll = () => {\n      if (timerRef.current) clearInterval(timerRef.current);\n      if (delayRef.current) clearTimeout(delayRef.current);\n      timerRef.current = null;\n      delayRef.current = null;\n    };\n\n    const start = () => {\n      phaseRef.current = 'scramble';\n      stepRef.current = 0;\n      setDisplay('\\u00A0'.repeat(text.length));\n\n      timerRef.current = setInterval(() => {\n        const step = stepRef.current;\n\n        if (phaseRef.current === 'scramble') {\n          const len = Math.min(step + 1, text.length);\n          const buf: string[] = [];\n          for (let i = 0; i < len; i++) {\n            buf.push(pickRandom(chars, i > 0 ? buf[i - 1] : undefined));\n          }\n          for (let i = len; i < text.length; i++) buf.push('\\u00A0');\n          setDisplay(buf.join(''));\n\n          if (step < text.length * 2 - 1) {\n            stepRef.current++;\n          } else {\n            phaseRef.current = 'reveal';\n            stepRef.current = 0;\n          }\n        } else {\n          const revealed = Math.floor(step / 2);\n          const buf: string[] = text\n            .slice(0, Math.min(revealed, text.length))\n            .split('');\n\n          if (revealed < text.length) {\n            buf.push(step % 2 === 0 ? '_' : pickRandom(chars));\n          }\n          while (buf.length < text.length) buf.push(pickRandom(chars));\n          setDisplay(buf.join(''));\n\n          if (step < text.length * 2 - 1) {\n            stepRef.current++;\n          } else {\n            setDisplay(text);\n            clearAll();\n          }\n        }\n      }, speed);\n    };\n\n    if (delay > 0) {\n      delayRef.current = setTimeout(start, delay * 1000);\n    } else {\n      start();\n    }\n\n    return clearAll;\n  }, [active, text, speed, delay, chars]);\n\n  return (\n    <span\n      ref={ref}\n      className={`inline-flex h-4.5 font-mono leading-5 font-medium ${className}`}\n    >\n      {display}\n    </span>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/odysseyui/text-reveal.tsx"
    }
  ],
  "type": "registry:ui"
}