{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"dependencies": [],
	"description": "Text that decrypts character by character with a scramble effect. Trigger on scroll, hover, or mount.",
	"files": [
		{
			"content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_CHARS = \"!@#$%^&*()_+-=[]{}|;:,.<>?\";\n\ninterface TextScrambleProps {\n  text: string;\n  className?: string;\n  chars?: string;\n  trigger?: \"inView\" | \"mount\" | \"hover\";\n  scrambleSpeed?: number;\n  cursorClassName?: string;\n  inViewThreshold?: number;\n  onComplete?: () => void;\n}\n\nexport function TextScramble({\n  text,\n  className,\n  chars = DEFAULT_CHARS,\n  trigger = \"inView\",\n  scrambleSpeed = 0,\n  cursorClassName,\n  inViewThreshold = 0.3,\n  onComplete,\n}: TextScrambleProps) {\n  const [display, setDisplay] = useState(\"\");\n  const [done, setDone] = useState(false);\n  const ref = useRef<HTMLSpanElement>(null);\n  const hasAnimatedRef = useRef(false);\n  const [inView, setInView] = useState(trigger === \"mount\");\n  const [hovered, setHovered] = useState(false);\n\n  useEffect(() => {\n    if (trigger !== \"inView\") {\n      return;\n    }\n    const el = ref.current;\n    if (!el) {\n      return;\n    }\n    const obs = new IntersectionObserver(([e]) => setInView(e?.isIntersecting ?? false), {\n      threshold: inViewThreshold,\n    });\n    obs.observe(el);\n    return () => obs.disconnect();\n  }, [trigger, inViewThreshold]);\n\n  const shouldRun = trigger === \"mount\" ? true : trigger === \"inView\" ? inView && !hasAnimatedRef.current : hovered;\n\n  useEffect(() => {\n    if (!shouldRun) {\n      return;\n    }\n    if (trigger === \"inView\" && hasAnimatedRef.current) {\n      return;\n    }\n    let id: number;\n    const resolve = (i: number) => {\n      if (i >= text.length) {\n        hasAnimatedRef.current = true;\n        setDone(true);\n        onComplete?.();\n        return;\n      }\n      const scramble = text\n        .split(\"\")\n        .map((c, j) => (j <= i ? c : (chars[Math.floor(Math.random() * chars.length)] ?? \"?\")))\n        .join(\"\");\n      setDisplay(scramble);\n      id = window.setTimeout(() => resolve(i + 1), scrambleSpeed);\n    };\n    id = window.setTimeout(() => resolve(0), scrambleSpeed);\n    return () => clearTimeout(id);\n  }, [shouldRun, trigger, text, chars, scrambleSpeed, onComplete]);\n\n  const content = (\n    <span ref={ref} className={cn(className)}>\n      {display}\n      {!done && (\n        <span className={cn(\"animate-pulse\", cursorClassName)} aria-hidden={true}>\n          |\n        </span>\n      )}\n    </span>\n  );\n\n  if (trigger === \"hover\") {\n    return (\n      // biome-ignore lint/a11y/useSemanticElements: hover trigger for decorative scramble, not a form group\n      <div className=\"inline\" role=\"group\" onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}>\n        {content}\n      </div>\n    );\n  }\n\n  return content;\n}\n",
			"path": "registry/new-york/text-scramble/text-scramble.tsx",
			"type": "registry:component"
		}
	],
	"name": "text-scramble",
	"title": "Text Scramble",
	"type": "registry:component"
}
