{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-decrypt",
  "title": "Text Decrypt",
  "description": "Scramble-to-decode text reveal with left-to-right character locking.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/text-decrypt/component.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nconst CHARSET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&@*+=\";\n\nexport function DecryptText({\n  text,\n  delay = 150,\n  stagger = 55,\n  ambient = 0,\n  className = \"\",\n  replayKey = 0,\n}: {\n  /** the text that resolves out of the scramble */\n  text: string;\n  /** ms before the first character locks */\n  delay?: number;\n  /** ms between character locks (left to right) */\n  stagger?: number;\n  /** ms between idle glitches re-scrambling a few characters; 0 = off */\n  ambient?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n  /** bump to replay the animation */\n  replayKey?: number;\n}) {\n  const chars = Array.from(text);\n  const [locked, setLocked] = useState(-1);\n  const [churn, setChurn] = useState<string[]>(chars.map(() => \"\"));\n  const [glitch, setGlitch] = useState<ReadonlySet<number>>(new Set());\n  const reduced = useRef(false);\n\n  const randomGlyph = useCallback(\n    () => CHARSET[Math.floor(Math.random() * CHARSET.length)],\n    []\n  );\n\n  useEffect(() => {\n    reduced.current = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    if (reduced.current) {\n      setLocked(chars.length - 1);\n      return;\n    }\n    setLocked(-1);\n    let raf = 0;\n    let frame = 0;\n    const start = performance.now();\n    const tick = (now: number) => {\n      const elapsed = now - start;\n      const lockedCount = Math.min(\n        chars.length - 1,\n        Math.floor((elapsed - delay) / stagger)\n      );\n      setLocked(lockedCount);\n      // churn unresolved glyphs every 3rd frame so they flicker, not strobe\n      if (frame++ % 3 === 0) {\n        setChurn(chars.map((c, i) => (i > lockedCount && c !== \" \" ? randomGlyph() : c)));\n      }\n      if (lockedCount < chars.length - 1) raf = requestAnimationFrame(tick);\n    };\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [text, delay, stagger, replayKey]);\n\n  // idle life: every `ambient` ms a few adjacent characters glitch and relock\n  useEffect(() => {\n    if (!ambient || reduced.current) return;\n    let churnId: ReturnType<typeof setInterval> | undefined;\n    const id = setInterval(() => {\n      const start = Math.floor(Math.random() * Math.max(1, chars.length - 3));\n      const span = new Set(\n        [start, start + 1, start + 2].filter((i) => i < chars.length && chars[i] !== \" \")\n      );\n      setGlitch(span);\n      let n = 0;\n      churnId = setInterval(() => {\n        setChurn((prev) =>\n          prev.map((c, i) => (span.has(i) ? randomGlyph() : c))\n        );\n        if (++n > 6) {\n          clearInterval(churnId);\n          setGlitch(new Set());\n        }\n      }, 55);\n    }, ambient);\n    return () => {\n      clearInterval(id);\n      if (churnId) clearInterval(churnId);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [ambient, text, replayKey]);\n\n  return (\n    <span aria-label={text} role=\"text\" className={`font-mono ${className}`}>\n      {chars.map((c, i) => {\n        const state =\n          i <= locked && !glitch.has(i)\n            ? \"locked\"\n            : i === locked + 1 || glitch.has(i)\n              ? \"resolving\"\n              : \"churning\";\n        return (\n          <span\n            key={i}\n            aria-hidden\n            className={\n              state === \"locked\"\n                ? \"text-foreground\"\n                : state === \"resolving\"\n                  ? \"text-foreground brightness-150\"\n                  : \"text-ns-muted/70\"\n            }\n          >\n            {c === \" \" ? \" \" : state === \"locked\" ? c : churn[i] || c}\n          </span>\n        );\n      })}\n    </span>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/text-decrypt.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)"
    },
    "light": {
      "ns-muted": "#4d4d4d"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "text",
      "reveal",
      "mono",
      "micro-interaction"
    ],
    "instruction": "A scramble-to-decode text reveal: monospace glyphs churn randomly and lock into the final string left to right with a per-character stagger, the currently resolving character brightened, unresolved characters dimmed. No layout jitter (mono charset), final text exposed via aria-label with churning spans aria-hidden, instant render under prefers-reduced-motion."
  },
  "type": "registry:ui"
}