{
  "name": "magic-particle-loader",
  "type": "registry:ui",
  "title": "Magic Particle Loader",
  "description": "High-performance HTML5 Canvas glowing particle vortex loader with dynamic orbital physics and hue cycles.",
  "dependencies": [
    "framer-motion"
  ],
  "files": [
    {
      "path": "registry/effects/magic-particle-loader/magic-particle-loader.tsx",
      "type": "registry:ui",
      "target": "components/ui/magic-particle-loader.tsx",
      "content": "import React, { useEffect, useRef, useCallback } from \"react\";\nimport { useInView } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\n\ninterface Particle {\n  radius: number;\n  x: number;\n  y: number;\n  angle: number;\n  speed: number;\n  accel: number;\n  decay: number;\n  life: number;\n}\n\nexport interface MagicParticleLoaderProps {\n  size?: number;\n  particleCount?: number;\n  speed?: number;\n  hueRange?: [number, number];\n  className?: string;\n}\n\nexport const MagicParticleLoader: React.FC<MagicParticleLoaderProps> = ({\n  size = 180,\n  particleCount = 1,\n  speed = 1,\n  hueRange = [210, 290],\n  className,\n}) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const animationRef = useRef<number | undefined>(undefined);\n  const particlesRef = useRef<Particle[]>([]);\n  const tickRef = useRef(0);\n  const globalAngleRef = useRef(0);\n  const globalRotationRef = useRef(0);\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const isInView = useInView(containerRef);\n  const visibilityRef = useRef(true);\n\n  useEffect(() => {\n    visibilityRef.current = isInView;\n  }, [isInView]);\n\n  const createParticle = useCallback(\n    (centerX: number, centerY: number, tick: number, minSize: number): Particle => {\n      return {\n        radius: 6,\n        x: centerX + (Math.cos(tick / 20) * minSize) / 2,\n        y: centerY + (Math.sin(tick / 20) * minSize) / 2,\n        angle: globalRotationRef.current + globalAngleRef.current,\n        speed: 0,\n        accel: 0.01,\n        decay: 0.012,\n        life: 1,\n      };\n    },\n    []\n  );\n\n  const stepParticle = useCallback(\n    (particle: Particle, index: number) => {\n      particle.speed += particle.accel;\n      particle.x += Math.cos(particle.angle) * particle.speed * speed;\n      particle.y += Math.sin(particle.angle) * particle.speed * speed;\n      particle.angle += Math.PI / 64;\n      particle.accel *= 1.01;\n      particle.life -= particle.decay;\n\n      if (particle.life <= 0) {\n        particlesRef.current.splice(index, 1);\n      }\n    },\n    [speed]\n  );\n\n  const drawParticle = useCallback(\n    (ctx: CanvasRenderingContext2D, particle: Particle, index: number, tick: number) => {\n      const hue =\n        hueRange[0] +\n        ((tick + particle.life * 100) % (hueRange[1] - hueRange[0]));\n      ctx.fillStyle = ctx.strokeStyle = `hsla(${hue}, 100%, 65%, ${particle.life * 0.9})`;\n\n      // Draw stroke trail to previous particle\n      ctx.beginPath();\n      if (particlesRef.current[index - 1]) {\n        ctx.moveTo(particle.x, particle.y);\n        ctx.lineTo(particlesRef.current[index - 1].x, particlesRef.current[index - 1].y);\n      }\n      ctx.stroke();\n\n      // Main particle orb\n      ctx.beginPath();\n      ctx.arc(\n        particle.x,\n        particle.y,\n        Math.max(0.001, particle.life * particle.radius),\n        0,\n        Math.PI * 2\n      );\n      ctx.fill();\n\n      // Micro glow flecks\n      const glowSize = Math.random() * 1.5;\n      const glowX = particle.x + (Math.random() - 0.5) * 30 * particle.life;\n      const glowY = particle.y + (Math.random() - 0.5) * 30 * particle.life;\n      ctx.fillRect(Math.floor(glowX), Math.floor(glowY), glowSize, glowSize);\n    },\n    [hueRange]\n  );\n\n  const animate = useCallback(() => {\n    if (!visibilityRef.current) {\n      animationRef.current = requestAnimationFrame(animate);\n      return;\n    }\n\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const rect = canvas.getBoundingClientRect();\n    const centerX = rect.width / 2;\n    const centerY = rect.height / 2;\n    const minSize = Math.min(rect.width, rect.height) * 0.5;\n\n    for (let i = 0; i < particleCount; i++) {\n      particlesRef.current.push(createParticle(centerX, centerY, tickRef.current, minSize));\n    }\n\n    particlesRef.current.forEach((particle, index) => {\n      stepParticle(particle, index);\n    });\n\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n    particlesRef.current.forEach((particle, index) => {\n      drawParticle(ctx, particle, index, tickRef.current);\n    });\n\n    globalRotationRef.current += (Math.PI / 6) * speed;\n    globalAngleRef.current += (Math.PI / 6) * speed;\n    tickRef.current++;\n\n    animationRef.current = requestAnimationFrame(animate);\n  }, [createParticle, stepParticle, drawParticle, particleCount, speed]);\n\n  const setupCanvas = useCallback(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const dpr = typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1;\n    canvas.width = size * dpr;\n    canvas.height = size * dpr;\n    canvas.style.width = `${size}px`;\n    canvas.style.height = `${size}px`;\n\n    ctx.scale(dpr, dpr);\n    ctx.globalCompositeOperation = \"lighter\";\n\n    particlesRef.current = [];\n    tickRef.current = 0;\n    globalAngleRef.current = 0;\n    globalRotationRef.current = 0;\n  }, [size]);\n\n  useEffect(() => {\n    setupCanvas();\n    animate();\n\n    return () => {\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current);\n      }\n    };\n  }, [setupCanvas, animate]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"flex items-center justify-center p-4\", className)}\n    >\n      <canvas\n        ref={canvasRef}\n        className=\"max-w-full max-h-full\"\n        style={{ width: size, height: size }}\n      />\n    </div>\n  );\n};\n\nexport default MagicParticleLoader;\n"
    }
  ],
  "$schema": "https://www.cooki-ui.design/schema/registry-item.json"
}