{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"shuffle-text","type":"registry:ui","title":"Shuffle Text","description":"Each letter spins like a slot-machine reel and lands on its final character, on view and on hover.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/shuffle-text.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useCallback, useEffect, useRef, useState, type CSSProperties } from \"react\";\n\nconst css = `@keyframes shuffle-text-spin{from{transform:translateY(0)}to{transform:translateY(var(--shuffle-to))}}`;\n\nexport interface ShuffleTextProps {\n  text: string;\n  /** Ms each reel spins before landing. */\n  duration?: number;\n  /** Ms between neighbouring reels starting. */\n  stagger?: number;\n  /** Random glyphs each reel passes before the final one. */\n  spins?: number;\n  /** Glyphs the reels are filled with. */\n  characters?: string;\n  /** Spin again on every hover. */\n  shuffleOnHover?: boolean;\n  className?: string;\n}\n\ntype Run = { id: number; reels: string[][] };\n\nexport function ShuffleText({\n  text,\n  duration = 900,\n  stagger = 45,\n  spins = 8,\n  characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$#&%\",\n  shuffleOnHover = true,\n  className,\n}: ShuffleTextProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const [run, setRun] = useState<Run | null>(null);\n  const busyUntil = useRef(0);\n  const chars = Array.from(text);\n\n  const shuffle = useCallback(() => {\n    const now = performance.now();\n    if (now < busyUntil.current) return;\n    const letters = Array.from(text);\n    const still = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    busyUntil.current = still ? 0 : now + duration + letters.length * stagger;\n    const pick = () => characters[Math.floor(Math.random() * characters.length)] ?? \"\";\n    setRun((r) => ({\n      id: (r?.id ?? 0) + 1,\n      reels: letters.map((c) => (still || c.trim() === \"\" ? [] : Array.from({ length: Math.max(1, spins) }, pick))),\n    }));\n  }, [text, duration, stagger, spins, characters]);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const io = new IntersectionObserver(([entry]) => {\n      if (entry.isIntersecting) {\n        busyUntil.current = 0;\n        shuffle();\n        io.disconnect();\n      }\n    });\n    io.observe(el);\n    return () => io.disconnect();\n  }, [shuffle]);\n\n  return (\n    <span\n      ref={ref}\n      className={className}\n      style={{ display: \"inline-block\", opacity: run ? 1 : 0 }}\n      onMouseEnter={shuffleOnHover ? shuffle : undefined}\n    >\n      <style href=\"shuffle-text\" precedence=\"default\">\n        {css}\n      </style>\n      <span className=\"sr-only\">{text}</span>\n      <span aria-hidden style={{ whiteSpace: \"pre\" }}>\n        {chars.map((c, i) => {\n          const reel = run?.reels[i] ?? [];\n          if (!reel.length)\n            return (\n              <span key={i} style={{ display: \"inline-block\" }}>\n                {c}\n              </span>\n            );\n          // Random glyphs, the final letter, and one spare below it for the overshoot.\n          const column = [...reel, c, reel[0]];\n          const to = (-reel.length / column.length) * 100;\n          return (\n            <span\n              key={`${run?.id}-${i}`}\n              style={{ position: \"relative\", display: \"inline-block\", overflow: \"hidden\", verticalAlign: \"top\" }}\n            >\n              <span style={{ visibility: \"hidden\" }}>{c}</span>\n              <span\n                style={\n                  {\n                    position: \"absolute\",\n                    inset: \"0 -0.5em auto\",\n                    display: \"flex\",\n                    flexDirection: \"column\",\n                    alignItems: \"center\",\n                    \"--shuffle-to\": `${to}%`,\n                    animation: `shuffle-text-spin ${duration}ms cubic-bezier(.2,.9,.25,1.03) ${i * stagger}ms both`,\n                  } as CSSProperties\n                }\n              >\n                {column.map((g, k) => (\n                  <span key={k} style={{ opacity: k === reel.length ? 1 : 0.4 }}>\n                    {g}\n                  </span>\n                ))}\n              </span>\n            </span>\n          );\n        })}\n      </span>\n    </span>\n  );\n}\n"}]}