{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "icon-cloud",
  "type": "registry:component",
  "title": "Icon cloud",
  "description": "A sphere of icons that turns slowly in 3D, scales and fades with depth, and can be dragged to spin with inertia.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/premium/icon-cloud.tsx",
      "type": "registry:ui",
      "target": "components/ui/icon-cloud.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport {\n  BellIcon,\n  BoxIcon,\n  BracesIcon,\n  BugIcon,\n  CalendarIcon,\n  ChartBarIcon,\n  ChartPieIcon,\n  CloudIcon,\n  CodeIcon,\n  CpuIcon,\n  CreditCardIcon,\n  DatabaseIcon,\n  GitBranchIcon,\n  GlobeIcon,\n  KeyIcon,\n  LayersIcon,\n  LockIcon,\n  MailIcon,\n  MessageSquareIcon,\n  PackageIcon,\n  ReceiptIcon,\n  RocketIcon,\n  ServerIcon,\n  SettingsIcon,\n  ShieldIcon,\n  SparklesIcon,\n  TerminalIcon,\n  UsersIcon,\n  WalletIcon,\n  ZapIcon,\n} from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * A sphere of icons that turns slowly in 3D, scales and fades with depth, and can be dragged to spin with inertia.\n * Items sit on a Fibonacci sphere so they spread evenly, and each is a real\n * element (lucide icons by default, or your children), so they stay crisp and\n * take token colours. The sphere turns on its own, keeps spinning after a\n * flick, pauses off screen and holds still for reduced motion. Arrow keys\n * turn it when it has focus.\n */\ntype IconCloudProps = {\n  /** Items to place on the sphere. Defaults to 30 lucide icons. */\n  children?: React.ReactNode\n  /** Diameter in pixels. Shrinks to fit a narrower container. */\n  size?: number\n  /** Idle spin speed. 1 is one turn every 25 seconds, 0 holds still until dragged. */\n  speed?: number\n  /** Size of each tile in pixels. */\n  itemSize?: number\n  /** Allow pointer drag and arrow keys to turn the sphere. */\n  interactive?: boolean\n  className?: string\n}\n\nconst DEFAULT_ICONS = [\n  CodeIcon, GitBranchIcon, DatabaseIcon, CloudIcon, ServerIcon, CpuIcon,\n  GlobeIcon, TerminalIcon, BracesIcon, BugIcon, BoxIcon, PackageIcon,\n  LayersIcon, ZapIcon, RocketIcon, ShieldIcon, LockIcon, KeyIcon,\n  MailIcon, BellIcon, CalendarIcon, ChartBarIcon, ChartPieIcon, CreditCardIcon,\n  ReceiptIcon, WalletIcon, UsersIcon, MessageSquareIcon, SettingsIcon, SparklesIcon,\n]\n\ntype Pt = { x: number; y: number; z: number }\n\n/** Evenly spread points on a unit sphere. Deterministic, so the server and client agree. */\nfunction fibonacciSphere(n: number): Pt[] {\n  const golden = Math.PI * (3 - Math.sqrt(5))\n  return Array.from({ length: n }, (_, i) => {\n    const y = n === 1 ? 0 : 1 - (i / (n - 1)) * 2\n    const r = Math.sqrt(Math.max(0, 1 - y * y))\n    const t = golden * i\n    return { x: Math.cos(t) * r, y, z: Math.sin(t) * r }\n  })\n}\n\n/** Rotate a point around Y then X and project it with a little perspective. */\nfunction project(p: Pt, rx: number, ry: number, radius: number) {\n  const cy = Math.cos(ry)\n  const sy = Math.sin(ry)\n  const cx = Math.cos(rx)\n  const sx = Math.sin(rx)\n  const x1 = p.x * cy + p.z * sy\n  const z1 = -p.x * sy + p.z * cy\n  const y2 = p.y * cx - z1 * sx\n  const z2 = p.y * sx + z1 * cx\n  const depth = (z2 + 1) / 2\n  const persp = 1 / (1.12 - 0.12 * z2)\n  return {\n    x: x1 * radius * persp,\n    y: y2 * radius * persp,\n    scale: 0.64 + 0.36 * depth,\n    opacity: 0.4 + 0.6 * depth,\n    z: Math.round(depth * 1000),\n  }\n}\n\nconst TILT = 0.38\n\nfunction IconCloud({ children, size = 360, speed = 1, itemSize = 48, interactive = true, className }: IconCloudProps) {\n  const reduce = useReducedMotion()\n  const boxRef = React.useRef<HTMLDivElement>(null)\n  const itemRefs = React.useRef<(HTMLSpanElement | null)[]>([])\n  const hintId = React.useId()\n\n  const items = React.useMemo(\n    () =>\n      children !== undefined\n        ? React.Children.toArray(children)\n        : DEFAULT_ICONS.map((Icon, i) => <Icon key={i} className=\"size-[22px]\" aria-hidden />),\n    [children]\n  )\n  const n = items.length\n  const points = React.useMemo(() => fibonacciSphere(n), [n])\n  const initialRadius = Math.max(0, size / 2 - itemSize / 2 - 4)\n\n  React.useEffect(() => {\n    const box = boxRef.current\n    if (!box || !n) return\n    const rot = { x: TILT, y: 0 }\n    const vel = { x: 0, y: 0 }\n    let radius = initialRadius\n    let raf = 0\n    let last = 0\n    let visible = true\n    let dragging = false\n    let lastX = 0\n    let lastY = 0\n    let lastT = 0\n    const idle = speed * 0.25\n    const gain = 0.0075\n\n    const measure = () => {\n      radius = Math.max(0, box.clientWidth / 2 - itemSize / 2 - 4)\n    }\n    const draw = () => {\n      for (let i = 0; i < n; i++) {\n        const el = itemRefs.current[i]\n        if (!el) continue\n        const q = project(points[i], rot.x, rot.y, radius)\n        el.style.transform = `translate(-50%, -50%) translate3d(${q.x.toFixed(1)}px, ${q.y.toFixed(1)}px, 0) scale(${q.scale.toFixed(3)})`\n        el.style.opacity = q.opacity.toFixed(3)\n        el.style.zIndex = String(q.z)\n      }\n    }\n    const step = (t: number) => {\n      const dt = last ? Math.min(0.05, (t - last) / 1000) : 0\n      last = t\n      if (!dragging && dt) {\n        const friction = Math.pow(0.08, dt)\n        vel.x *= friction\n        vel.y *= friction\n        rot.y += (idle + vel.y) * dt\n        rot.x += vel.x * dt\n      }\n      draw()\n      raf = requestAnimationFrame(step)\n    }\n    const stop = () => {\n      cancelAnimationFrame(raf)\n      raf = 0\n      last = 0\n    }\n    const sync = () => {\n      const run = !reduce && visible && !document.hidden\n      if (run && !raf) raf = requestAnimationFrame(step)\n      if (!run && raf) stop()\n    }\n\n    const onDown = (e: PointerEvent) => {\n      if (!interactive || e.button !== 0) return\n      dragging = true\n      vel.x = 0\n      vel.y = 0\n      lastX = e.clientX\n      lastY = e.clientY\n      lastT = e.timeStamp\n      box.setPointerCapture(e.pointerId)\n      box.dataset.dragging = \"true\"\n    }\n    const onMove = (e: PointerEvent) => {\n      if (!dragging) return\n      const dx = e.clientX - lastX\n      const dy = e.clientY - lastY\n      const dt = Math.max(1, e.timeStamp - lastT) / 1000\n      lastX = e.clientX\n      lastY = e.clientY\n      lastT = e.timeStamp\n      rot.y += dx * gain\n      rot.x -= dy * gain\n      // Smooth the flick velocity so one jittery event cannot launch the sphere.\n      vel.y = vel.y * 0.5 + Math.max(-10, Math.min(10, (dx * gain) / dt)) * 0.5\n      vel.x = vel.x * 0.5 + Math.max(-10, Math.min(10, (-dy * gain) / dt)) * 0.5\n      if (reduce || !raf) draw()\n    }\n    const onUp = (e: PointerEvent) => {\n      if (!dragging) return\n      dragging = false\n      delete box.dataset.dragging\n      if (box.hasPointerCapture(e.pointerId)) box.releasePointerCapture(e.pointerId)\n      // A still pointer at release means no flick.\n      if (e.timeStamp - lastT > 80) {\n        vel.x = 0\n        vel.y = 0\n      }\n      if (reduce) {\n        vel.x = 0\n        vel.y = 0\n      }\n    }\n    const onKey = (e: KeyboardEvent) => {\n      if (!interactive) return\n      const k = e.key\n      if (k !== \"ArrowLeft\" && k !== \"ArrowRight\" && k !== \"ArrowUp\" && k !== \"ArrowDown\") return\n      e.preventDefault()\n      if (reduce) {\n        if (k === \"ArrowLeft\") rot.y -= 0.3\n        if (k === \"ArrowRight\") rot.y += 0.3\n        if (k === \"ArrowUp\") rot.x += 0.3\n        if (k === \"ArrowDown\") rot.x -= 0.3\n        draw()\n        return\n      }\n      if (k === \"ArrowLeft\") vel.y -= 2.4\n      if (k === \"ArrowRight\") vel.y += 2.4\n      if (k === \"ArrowUp\") vel.x += 2.4\n      if (k === \"ArrowDown\") vel.x -= 2.4\n    }\n\n    measure()\n    draw()\n    const ro = new ResizeObserver(() => {\n      measure()\n      if (reduce || !raf) draw()\n    })\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting\n      sync()\n    })\n    ro.observe(box)\n    io.observe(box)\n    box.addEventListener(\"pointerdown\", onDown)\n    box.addEventListener(\"pointermove\", onMove)\n    box.addEventListener(\"pointerup\", onUp)\n    box.addEventListener(\"pointercancel\", onUp)\n    box.addEventListener(\"keydown\", onKey)\n    document.addEventListener(\"visibilitychange\", sync)\n    sync()\n\n    return () => {\n      stop()\n      ro.disconnect()\n      io.disconnect()\n      box.removeEventListener(\"pointerdown\", onDown)\n      box.removeEventListener(\"pointermove\", onMove)\n      box.removeEventListener(\"pointerup\", onUp)\n      box.removeEventListener(\"pointercancel\", onUp)\n      box.removeEventListener(\"keydown\", onKey)\n      document.removeEventListener(\"visibilitychange\", sync)\n    }\n  }, [n, points, speed, itemSize, interactive, reduce, initialRadius])\n\n  return (\n    <div\n      ref={boxRef}\n      data-slot=\"icon-cloud\"\n      data-tier=\"premium\"\n      role=\"group\"\n      aria-label={`Rotating cloud of ${n} icons`}\n      aria-describedby={interactive ? hintId : undefined}\n      tabIndex={interactive ? 0 : undefined}\n      className={cn(\n        \"relative mx-auto aspect-square w-full touch-pan-y rounded-full outline-none select-none\",\n        \"focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-4 focus-visible:ring-offset-bg\",\n        interactive && \"cursor-grab data-[dragging=true]:cursor-grabbing\",\n        className\n      )}\n      style={{ maxWidth: size }}\n    >\n      {items.map((item, i) => {\n        const q = project(points[i], TILT, 0, initialRadius)\n        return (\n          <span\n            key={i}\n            ref={(el) => {\n              itemRefs.current[i] = el\n            }}\n            aria-hidden\n            data-slot=\"icon-cloud-item\"\n            className=\"absolute top-1/2 left-1/2 grid place-items-center rounded-xl border border-border bg-raised text-fg shadow-raised will-change-transform\"\n            style={{\n              width: itemSize,\n              height: itemSize,\n              transform: `translate(-50%, -50%) translate3d(${q.x.toFixed(1)}px, ${q.y.toFixed(1)}px, 0) scale(${q.scale.toFixed(3)})`,\n              opacity: q.opacity.toFixed(3),\n              zIndex: q.z,\n            }}\n          >\n            {item}\n          </span>\n        )\n      })}\n      {interactive ? (\n        <span id={hintId} className=\"sr-only\">\n          Drag to turn the sphere, or use the arrow keys while it has focus.\n        </span>\n      ) : null}\n    </div>\n  )\n}\n\nexport { IconCloud }\nexport type { IconCloudProps }\n"
    }
  ]
}
