{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ascii-glitch-ripple",
  "type": "registry:ui",
  "dependencies": [],
  "files": [
    {
      "path": "components/ui/ascii-glitch-ripple.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// Constants for wave animation behavior\nconst WAVE_THRESH = 3;\nconst CHAR_MULT = 3;\nconst ANIM_STEP = 40;\nconst WAVE_BUF = 5;\n\nexport interface AsciiGlitchRippleProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\n  /**\n   * The text to display and animate.\n   */\n  children: string;\n  /**\n   * The HTML element or component to render as.\n   * @default \"a\"\n   */\n  as?: any;\n  /**\n   * Additional CSS classes.\n   */\n  className?: string;\n  /**\n   * Duration of each ripple wave in milliseconds.\n   * @default 1000\n   */\n  dur?: number;\n  /**\n   * Character set to scramble through during the ripple wave.\n   * @default '.,·-─~+:;=*π\"\"┐┌┘┴┬╗╔╝╚╬╠╣╩╦║░▒▓█▄▀▌▐■!?&#$@0123456789*'\n   */\n  chars?: string;\n  /**\n   * Whether to preserve space characters or scramble them too.\n   * @default true\n   */\n  preserveSpaces?: boolean;\n  /**\n   * The spread of the ripple wave. Larger numbers mean wider waves.\n   * @default 1.0\n   */\n  spread?: number;\n  [key: string]: any;\n}\n\nexport function AsciiGlitchRipple({\n  children,\n  as = \"a\",\n  className,\n  dur = 1000,\n  chars = '.,·-─~+:;=*π\"\"┐┌┘┴┬╗╔╝╚╬╠╣╩╦║░▒▓█▄▀▌▐■!?&#$@0123456789*',\n  preserveSpaces = true,\n  spread = 1.0,\n  ...props\n}: AsciiGlitchRippleProps) {\n  const Component = as;\n  const elRef = useRef<any>(null);\n\n  // Use a mutable ref to store animation state, preventing unnecessary React renders\n  const stateRef = useRef({\n    origTxt: children,\n    origChars: children.split(\"\"),\n    isAnim: false,\n    cursorPos: 0,\n    waves: [] as Array<{ startPos: number; startTime: number; id: number }>,\n    animId: null as number | null,\n    isHover: false,\n    origW: null as number | null,\n    dur,\n    chars,\n    preserveSpaces,\n    spread,\n  });\n\n  // Keep internal mutable state updated when props change\n  useEffect(() => {\n    stateRef.current.origTxt = children;\n    stateRef.current.origChars = children.split(\"\");\n    stateRef.current.dur = dur;\n    stateRef.current.chars = chars;\n    stateRef.current.preserveSpaces = preserveSpaces;\n    stateRef.current.spread = spread;\n\n    // Reset layout widths if text changes dynamically\n    if (stateRef.current.origW !== null && elRef.current) {\n      elRef.current.style.width = \"\";\n      stateRef.current.origW = null;\n    }\n\n    if (!stateRef.current.isAnim && elRef.current) {\n      elRef.current.textContent = children;\n    }\n  }, [children, dur, chars, preserveSpaces, spread]);\n\n  useEffect(() => {\n    const el = elRef.current;\n    if (!el) return;\n\n    // Initialize content\n    el.textContent = children;\n\n    const updateCursorPos = (e: MouseEvent) => {\n      const rect = el.getBoundingClientRect();\n      const x = e.clientX - rect.left;\n      const len = stateRef.current.origTxt.length;\n      const pos = Math.round((x / rect.width) * len);\n      stateRef.current.cursorPos = Math.max(0, Math.min(pos, len - 1));\n    };\n\n    const stop = () => {\n      el.textContent = stateRef.current.origTxt;\n      el.classList.remove(\"as\");\n\n      // Restore natural width layout\n      if (stateRef.current.origW !== null) {\n        el.style.width = \"\";\n        stateRef.current.origW = null;\n      }\n      stateRef.current.isAnim = false;\n      if (stateRef.current.animId) {\n        cancelAnimationFrame(stateRef.current.animId);\n        stateRef.current.animId = null;\n      }\n    };\n\n    const start = () => {\n      if (stateRef.current.isAnim) return;\n\n      // Lock current width to prevent layout shifts during ASCII scrambling\n      if (stateRef.current.origW === null) {\n        stateRef.current.origW = el.getBoundingClientRect().width;\n        el.style.width = `${stateRef.current.origW}px`;\n      }\n\n      stateRef.current.isAnim = true;\n      el.classList.add(\"as\");\n\n      const animate = () => {\n        const t = Date.now();\n\n        // Evict finished waves\n        stateRef.current.waves = stateRef.current.waves.filter(\n          (w) => t - w.startTime < stateRef.current.dur\n        );\n\n        if (stateRef.current.waves.length === 0) {\n          stop();\n          return;\n        }\n\n        // Apply visual scramble\n        el.textContent = genScrambledTxt(t);\n        stateRef.current.animId = requestAnimationFrame(animate);\n      };\n\n      stateRef.current.animId = requestAnimationFrame(animate);\n    };\n\n    const startWave = () => {\n      stateRef.current.waves.push({\n        startPos: stateRef.current.cursorPos,\n        startTime: Date.now(),\n        id: Math.random(),\n      });\n\n      if (!stateRef.current.isAnim) start();\n    };\n\n    const calcWaveEffect = (charIdx: number, t: number) => {\n      let shouldAnim = false;\n      let resultChar = stateRef.current.origChars[charIdx];\n\n      for (const w of stateRef.current.waves) {\n        const age = t - w.startTime;\n        const prog = Math.min(age / stateRef.current.dur, 1);\n        const dist = Math.abs(charIdx - w.startPos);\n        const maxDist = Math.max(w.startPos, stateRef.current.origChars.length - w.startPos - 1);\n        const rad = (prog * (maxDist + WAVE_BUF)) / stateRef.current.spread;\n\n        if (dist <= rad) {\n          shouldAnim = true;\n          const intens = Math.max(0, rad - dist);\n\n          // Wave distortion characters\n          if (intens <= WAVE_THRESH && intens > 0) {\n            const index =\n              (dist * CHAR_MULT + Math.floor(age / ANIM_STEP)) % stateRef.current.chars.length;\n            resultChar = stateRef.current.chars[index];\n          }\n        }\n      }\n\n      return { shouldAnim, char: resultChar };\n    };\n\n    const genScrambledTxt = (t: number) =>\n      stateRef.current.origChars\n        .map((char, i) => {\n          if (stateRef.current.preserveSpaces && char === \" \") return \" \";\n          const res = calcWaveEffect(i, t);\n          return res.shouldAnim ? res.char : char;\n        })\n        .join(\"\");\n\n    const handleEnter = (e: MouseEvent) => {\n      stateRef.current.isHover = true;\n      updateCursorPos(e);\n      startWave();\n    };\n\n    const handleMove = (e: MouseEvent) => {\n      if (!stateRef.current.isHover) return;\n      const old = stateRef.current.cursorPos;\n      updateCursorPos(e);\n      if (stateRef.current.cursorPos !== old) startWave();\n    };\n\n    const handleLeave = () => {\n      stateRef.current.isHover = false;\n    };\n\n    el.addEventListener(\"mouseenter\", handleEnter);\n    el.addEventListener(\"mousemove\", handleMove);\n    el.addEventListener(\"mouseleave\", handleLeave);\n\n    return () => {\n      el.removeEventListener(\"mouseenter\", handleEnter);\n      el.removeEventListener(\"mousemove\", handleMove);\n      el.removeEventListener(\"mouseleave\", handleLeave);\n      if (stateRef.current.animId) {\n        cancelAnimationFrame(stateRef.current.animId);\n      }\n    };\n  }, [children]);\n\n  return (\n    <Component\n      ref={elRef}\n      className={cn(\n        \"cursor-pointer select-none relative inline-block transition-colors duration-200\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nexport default AsciiGlitchRipple;\n",
      "type": "registry:ui",
      "target": "components/ui/ascii-glitch-ripple.tsx"
    }
  ]
}
