{
  "$schema": "https://www.cooki-ui.design/schema/registry-item.json",
  "name": "particle-vortex-tunnel",
  "type": "registry:ui",
  "title": "Hypnotic Particle Vortex Tunnel",
  "description": "HTML5 Canvas warp-speed vortex tunnel accelerating star particles inward toward a deep gravity well.",
  "category": "3d",
  "dependencies": [],
  "files": [
    {
      "path": "registry/3d/particle-vortex-tunnel/particle-vortex-tunnel.tsx",
      "type": "registry:ui",
      "target": "components/ui/particle-vortex-tunnel.tsx",
      "content": "import React, { useRef, useEffect } from 'react';\n\nexport const ParticleVortexTunnel: React.FC = () => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\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 animId: number;\n    let isVisible = true;\n    const stars: { x: number; y: number; z: number }[] = [];\n    const count = 120;\n\n    for (let i = 0; i < count; i++) {\n      stars.push({\n        x: (Math.random() - 0.5) * 400,\n        y: (Math.random() - 0.5) * 400,\n        z: Math.random() * 400,\n      });\n    }\n\n    const render = () => {\n      if (!isVisible) return;\n      ctx.fillStyle = 'rgba(5, 5, 8, 0.25)';\n      ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n      const cx = canvas.width / 2;\n      const cy = canvas.height / 2;\n\n      for (let s of stars) {\n        s.z -= 2.2;\n        if (s.z <= 0) s.z = 400;\n\n        const k = 200 / s.z;\n        const px = s.x * k + cx;\n        const py = s.y * k + cy;\n\n        if (px >= 0 && px <= canvas.width && py >= 0 && py <= canvas.height) {\n          const size = Math.max(0.8, (1 - s.z / 400) * 3);\n          const alpha = 1 - s.z / 400;\n\n          ctx.fillStyle = `rgba(129, 140, 248, ${alpha})`;\n          ctx.beginPath();\n          ctx.arc(px, py, size, 0, Math.PI * 2);\n          ctx.fill();\n        }\n      }\n\n      animId = requestAnimationFrame(render);\n    };\n\n    animId = requestAnimationFrame(render);\n\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    const observer = new IntersectionObserver(([entry]) => {\n      const wasVisible = isVisible;\n      isVisible = entry.isIntersecting;\n      if (isVisible && !wasVisible) {\n        animId = requestAnimationFrame(render);\n      }\n    }, { threshold: 0.05 });\n    observer.observe(canvas);\n\n    return () => {\n      cancelAnimationFrame(animId);\n      observer.disconnect();\n      resizeObserver.disconnect();\n    };\n  }, []);\n\n  return (\n    <div className=\"relative w-full h-56 rounded-2xl overflow-hidden bg-black border border-white/10 flex items-center justify-center\">\n      <canvas ref={canvasRef} className=\"w-full h-full\" />\n      <span className=\"absolute z-10 bottom-3 px-3 py-1 rounded-full bg-zinc-900/80 text-[10px] font-mono text-zinc-400 border border-white/10\">\n        WARP TUNNEL VELOCITY\n      </span>\n    </div>\n  );\n};\n"
    }
  ]
}