{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"ring-type","type":"registry:ui","title":"Ring Type","description":"Text wrapped around a slowly spinning ring that speeds up, slows down or stops when hovered, with optional content in the middle.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/ring-type.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, type ReactNode } from \"react\";\n\nconst HOVER = { none: 1, speedUp: 4, slowDown: 0.25, pause: 0, goBonkers: 14 } as const;\n\nexport interface RingTypeProps {\n  text?: string;\n  /** Seconds per full turn. */\n  spinDuration?: number;\n  /** What hovering does to the spin. */\n  onHover?: keyof typeof HOVER;\n  /** Diameter in px. */\n  size?: number;\n  /** Optional content in the middle of the ring. */\n  children?: ReactNode;\n  className?: string;\n}\n\nexport function RingType({\n  text = \"GLINT • MOTION • COMPONENTS • \",\n  spinDuration = 20,\n  onHover = \"speedUp\",\n  size = 220,\n  children,\n  className,\n}: RingTypeProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const ring = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const el = root.current!;\n    const r = ring.current!;\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n    let raf = 0;\n    let last = 0;\n    let angle = 0;\n    let mult = 1;\n    let target = 1;\n    let visible = true;\n\n    const loop = (now: number) => {\n      const dt = last ? Math.min((now - last) / 1000, 0.05) : 0;\n      last = now;\n      mult += (target - mult) * Math.min(1, dt * 4);\n      angle = (angle + (360 / Math.max(spinDuration, 0.1)) * mult * dt) % 360;\n      const bonkers = onHover === \"goBonkers\" ? 1 - Math.min(1, (mult - 1) / 13) * 0.12 : 1;\n      r.style.transform = `rotate(${angle}deg) scale(${bonkers})`;\n      raf = visible ? requestAnimationFrame(loop) : 0;\n    };\n    const enter = () => (target = HOVER[onHover] ?? 1);\n    const leave = () => (target = 1);\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible && !raf) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    });\n    io.observe(el);\n    el.addEventListener(\"pointerenter\", enter);\n    el.addEventListener(\"pointerleave\", leave);\n    return () => {\n      cancelAnimationFrame(raf);\n      io.disconnect();\n      el.removeEventListener(\"pointerenter\", enter);\n      el.removeEventListener(\"pointerleave\", leave);\n    };\n  }, [spinDuration, onHover]);\n\n  const chars = Array.from(text);\n  return (\n    <div\n      ref={root}\n      className={className}\n      style={{ position: \"relative\", width: size, height: size, borderRadius: \"50%\", flexShrink: 0 }}\n    >\n      <span className=\"sr-only\">{text}</span>\n      <div ref={ring} aria-hidden style={{ position: \"absolute\", inset: 0, willChange: \"transform\" }}>\n        {chars.map((c, i) => (\n          <span\n            key={i}\n            style={{\n              position: \"absolute\",\n              left: \"50%\",\n              top: 0,\n              height: \"50%\",\n              fontSize: size * 0.085,\n              lineHeight: 1,\n              transformOrigin: \"50% 100%\",\n              transform: `translateX(-50%) rotate(${(360 / chars.length) * i}deg)`,\n              whiteSpace: \"pre\",\n            }}\n          >\n            {c}\n          </span>\n        ))}\n      </div>\n      {children && (\n        <div style={{ position: \"absolute\", inset: 0, display: \"grid\", placeItems: \"center\" }}>{children}</div>\n      )}\n    </div>\n  );\n}\n"}]}