{
  "$schema": "https://www.cooki-ui.design/schema/registry-item.json",
  "name": "orbital-particle-sphere",
  "type": "registry:ui",
  "title": "Orbital Particle Sphere",
  "description": "3D perspective particle globe rotating along equatorial and polar orbital planes with cursor velocity inertia and depth illumination.",
  "category": "animations",
  "dependencies": [
    "@phosphor-icons/react"
  ],
  "files": [
    {
      "path": "registry/animations/orbital-particle-sphere/orbital-particle-sphere.tsx",
      "type": "registry:ui",
      "target": "components/ui/orbital-particle-sphere.tsx",
      "content": "import React, { useRef, useEffect, useState } from 'react';\nimport { Globe, Compass, ArrowsClockwise } from '@phosphor-icons/react';\n\nexport interface OrbitalParticleSphereProps {\n  particleCount?: number;\n  rotationSpeed?: number;\n  colorTheme?: 'cyan' | 'emerald' | 'violet' | 'amber';\n  interactive?: boolean;\n  className?: string;\n}\n\ninterface Point3D {\n  x: number;\n  y: number;\n  z: number;\n}\n\nexport const OrbitalParticleSphere: React.FC<OrbitalParticleSphereProps> = ({\n  particleCount = 140,\n  rotationSpeed = 1,\n  colorTheme = 'cyan',\n  interactive = true,\n  className = '',\n}) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const animIdRef = useRef<number | null>(null);\n  const rotYRef = useRef(0);\n  const rotXRef = useRef(0.2);\n  const velocityRef = useRef({ vx: 0.006 * rotationSpeed, vy: 0.002 * rotationSpeed });\n  const isDraggingRef = useRef(false);\n  const lastMouseRef = useRef({ x: 0, y: 0 });\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext('2d');\n    if (!ctx) return;\n\n    let isVisible = true;\n\n    // Responsive Canvas Resize Observer\n    const handleResize = () => {\n      const rect = canvas.getBoundingClientRect();\n      if (rect.width > 0 && rect.height > 0) {\n        canvas.width = rect.width;\n        canvas.height = rect.height;\n      }\n    };\n    handleResize();\n    const resizeObserver = new ResizeObserver(handleResize);\n    resizeObserver.observe(canvas);\n\n    // Generate sphere points using Fibonacci Golden Spiral\n    const points: Point3D[] = [];\n    const count = Math.max(60, Math.min(particleCount, 240));\n    const phi = Math.PI * (3 - Math.sqrt(5)); // Golden angle\n\n    for (let i = 0; i < count; i++) {\n      const y = 1 - (i / (count - 1)) * 2; // -1 to 1\n      const radiusAtY = Math.sqrt(1 - y * y);\n      const theta = phi * i;\n\n      points.push({\n        x: Math.cos(theta) * radiusAtY,\n        y: y,\n        z: Math.sin(theta) * radiusAtY,\n      });\n    }\n\n    const themeColors = {\n      cyan: { front: '#38bdf8', back: '#0369a1', glow: 'rgba(56,189,248,0.3)' },\n      emerald: { front: '#34d399', back: '#065f46', glow: 'rgba(52,211,153,0.3)' },\n      violet: { front: '#a78bfa', back: '#5b21b6', glow: 'rgba(167,139,250,0.3)' },\n      amber: { front: '#fbbf24', back: '#92400e', glow: 'rgba(251,191,36,0.3)' },\n    }[colorTheme];\n\n    const render = () => {\n      if (!isVisible) return;\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n      // Apply angular rotation & velocity inertia\n      if (!isDraggingRef.current) {\n        rotYRef.current += velocityRef.current.vx;\n        rotXRef.current += velocityRef.current.vy;\n      }\n\n      const sphereRadius = Math.min(canvas.width, canvas.height) * 0.38;\n      const cx = canvas.width / 2;\n      const cy = canvas.height / 2;\n      const fov = 340;\n\n      const cosY = Math.cos(rotYRef.current);\n      const sinY = Math.sin(rotYRef.current);\n      const cosX = Math.cos(rotXRef.current);\n      const sinX = Math.sin(rotXRef.current);\n\n      // Project and sort points by Z depth for accurate rendering\n      const projected = points.map((p) => {\n        // Rotate Y\n        const x1 = p.x * cosY - p.z * sinY;\n        const z1 = p.z * cosY + p.x * sinY;\n\n        // Rotate X\n        const y2 = p.y * cosX - z1 * sinX;\n        const z2 = z1 * cosX + p.y * sinX;\n\n        const scale = fov / (fov + z2 * sphereRadius);\n        const px = cx + x1 * sphereRadius * scale;\n        const py = cy + y2 * sphereRadius * scale;\n\n        return { px, py, z: z2, scale };\n      });\n\n      // Sort back-to-front\n      projected.sort((a, b) => a.z - b.z);\n\n      // Draw projected particle nodes\n      projected.forEach((pt) => {\n        const isFront = pt.z > 0;\n        const depthAlpha = Math.max(0.12, Math.min(1, (pt.z + 1) / 2));\n        const size = Math.max(1, (pt.z + 1.2) * 2.2);\n\n        ctx.save();\n        ctx.globalAlpha = depthAlpha;\n        ctx.fillStyle = isFront ? themeColors.front : themeColors.back;\n\n        if (isFront && size > 2.2) {\n          ctx.shadowColor = themeColors.front;\n          ctx.shadowBlur = 6;\n        }\n\n        ctx.beginPath();\n        ctx.arc(pt.px, pt.py, size, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.restore();\n      });\n\n      animIdRef.current = requestAnimationFrame(render);\n    };\n\n    animIdRef.current = requestAnimationFrame(render);\n\n    // Pause loop when offscreen\n    const observer = new IntersectionObserver(([entry]) => {\n      const wasVisible = isVisible;\n      isVisible = entry.isIntersecting;\n      if (isVisible && !wasVisible) {\n        animIdRef.current = requestAnimationFrame(render);\n      }\n    }, { threshold: 0.05 });\n    observer.observe(canvas);\n\n    return () => {\n      if (animIdRef.current) cancelAnimationFrame(animIdRef.current);\n      observer.disconnect();\n      resizeObserver.disconnect();\n    };\n  }, [particleCount, rotationSpeed, colorTheme]);\n\n  // Interactive Drag Controls\n  const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {\n    if (!interactive) return;\n    isDraggingRef.current = true;\n    lastMouseRef.current = { x: e.clientX, y: e.clientY };\n  };\n\n  const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {\n    if (!isDraggingRef.current || !interactive) return;\n    const deltaX = e.clientX - lastMouseRef.current.x;\n    const deltaY = e.clientY - lastMouseRef.current.y;\n    lastMouseRef.current = { x: e.clientX, y: e.clientY };\n\n    rotYRef.current += deltaX * 0.008;\n    rotXRef.current += deltaY * 0.008;\n  };\n\n  const handleMouseUp = () => {\n    isDraggingRef.current = false;\n  };\n\n  return (\n    <div className={`relative w-full h-72 sm:h-80 rounded-3xl overflow-hidden bg-black border border-white/10 flex items-center justify-center select-none ${className}`}>\n      {/* Background radial glow */}\n      <div className=\"absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(56,189,248,0.08)_0%,transparent_70%)] pointer-events-none\" />\n\n      {/* Responsive 3D Canvas */}\n      <canvas\n        ref={canvasRef}\n        onMouseDown={handleMouseDown}\n        onMouseMove={handleMouseMove}\n        onMouseUp={handleMouseUp}\n        onMouseLeave={handleMouseUp}\n        className=\"w-full h-full cursor-grab active:cursor-grabbing\"\n      />\n\n      {/* Bottom Telemetry Overlay */}\n      <div className=\"absolute bottom-3 left-4 right-4 flex items-center justify-between text-[10px] font-mono text-zinc-400 pointer-events-none\">\n        <div className=\"flex items-center gap-1.5 bg-zinc-900/80 px-2.5 py-1 rounded-full border border-white/10 backdrop-blur-md\">\n          <Globe size={13} className=\"text-cyan-400\" />\n          <span>FIBONACCI SPHERICAL MESH</span>\n        </div>\n        <span className=\"text-zinc-500 hidden sm:inline\">DRAG CANVAS TO ROTATE ORBIT</span>\n      </div>\n    </div>\n  );\n};\n"
    }
  ]
}