{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loader-terminal-decode",
  "title": "Terminal Decode Loader",
  "description": "Terminal-style text decoder that cycles through random characters before resolving",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/loaders/loader-terminal-decode/components/elements/loader-terminal-decode.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst CHARSETS = {\n  symbols: \"!@#$%^&*()_+-=[]{}|;:,.<>?/~\",\n  binary: \"01\",\n  hex: \"0123456789ABCDEF\",\n};\n\ntype DisplayChar = {\n  char: string;\n  resolved: boolean;\n};\n\nexport type LoaderTerminalDecodeProps = {\n  text?: string;\n  speed?: number;\n  charset?: \"symbols\" | \"binary\" | \"hex\";\n  loop?: boolean;\n  className?: string;\n};\n\nexport function LoaderTerminalDecode({\n  text = \"LOADING\",\n  speed = 30,\n  charset = \"symbols\",\n  loop = true,\n  className,\n}: LoaderTerminalDecodeProps) {\n  const [displayChars, setDisplayChars] = useState<DisplayChar[]>([]);\n  const rafRef = useRef<number | null>(null);\n  const startTimeRef = useRef<number>(0);\n  const lastResolveTimeRef = useRef<number>(0);\n  const resolvedCountRef = useRef<number>(0);\n  const isWaitingRef = useRef<boolean>(false);\n  const waitStartRef = useRef<number>(0);\n\n  const chars = CHARSETS[charset];\n\n  const getRandomChar = useCallback(\n    () => chars[Math.floor(Math.random() * chars.length)],\n    [chars],\n  );\n\n  const reset = useCallback(() => {\n    setDisplayChars(\n      text.split(\"\").map((char) => ({\n        char: char === \" \" ? \" \" : getRandomChar(),\n        resolved: char === \" \",\n      })),\n    );\n    resolvedCountRef.current = text.split(\"\").filter((c) => c === \" \").length;\n    lastResolveTimeRef.current = 0;\n    isWaitingRef.current = false;\n  }, [text, getRandomChar]);\n\n  useEffect(() => {\n    reset();\n    startTimeRef.current = performance.now();\n\n    const animate = (currentTime: number) => {\n      const elapsed = currentTime - startTimeRef.current;\n\n      if (isWaitingRef.current) {\n        if (currentTime - waitStartRef.current >= 1500) {\n          reset();\n          startTimeRef.current = currentTime;\n          lastResolveTimeRef.current = 0;\n        }\n        rafRef.current = requestAnimationFrame(animate);\n        return;\n      }\n\n      const shouldResolveNext =\n        resolvedCountRef.current < text.length &&\n        elapsed - lastResolveTimeRef.current >= speed;\n\n      setDisplayChars((prev) =>\n        prev.map((item, index) => {\n          if (item.resolved) {\n            return item;\n          }\n\n          if (text[index] === \" \") {\n            return { char: \" \", resolved: true };\n          }\n\n          if (shouldResolveNext && index === resolvedCountRef.current) {\n            lastResolveTimeRef.current = elapsed;\n            resolvedCountRef.current++;\n            return { char: text[index], resolved: true };\n          }\n\n          return { char: getRandomChar(), resolved: false };\n        }),\n      );\n\n      if (resolvedCountRef.current >= text.length) {\n        if (loop) {\n          isWaitingRef.current = true;\n          waitStartRef.current = currentTime;\n        }\n      }\n\n      rafRef.current = requestAnimationFrame(animate);\n    };\n\n    rafRef.current = requestAnimationFrame(animate);\n\n    return () => {\n      if (rafRef.current !== null) {\n        cancelAnimationFrame(rafRef.current);\n      }\n    };\n  }, [text, speed, loop, reset, getRandomChar]);\n\n  return (\n    <output\n      data-slot=\"loader-terminal-decode\"\n      aria-live=\"polite\"\n      aria-label={text}\n      className={cn(\"inline-flex font-mono\", className)}\n    >\n      <span className=\"sr-only\">{text}</span>\n      <span aria-hidden=\"true\" className=\"inline-flex\">\n        {displayChars.map((item, i) => (\n          <span\n            // biome-ignore lint/suspicious/noArrayIndexKey: stable list derived from fixed text length\n            key={i}\n            className={cn(\n              \"inline-block w-[1ch] text-center transition-colors duration-150\",\n              item.resolved ? \"text-foreground\" : \"text-muted-foreground\",\n            )}\n          >\n            {item.char}\n          </span>\n        ))}\n      </span>\n    </output>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Cipher-cracking text decoder with configurable charset (symbols, binary, hex), speed, and loop behavior. Monospace terminal aesthetic.",
  "categories": [
    "loaders"
  ],
  "type": "registry:block"
}