{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "particles",
  "title": "Particles",
  "description": "Drifting, twinkling particle field on a lightweight canvas.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/particles.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface ParticlesProps {\n  className?: string;\n  quantity?: number;\n  /** \"r, g, b\" string. Defaults to the inherited text color, so it adapts to the theme. */\n  color?: string;\n}\n\ninterface Particle {\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  size: number;\n  alpha: number;\n  twinkle: number;\n}\n\n/**\n * Drifting, twinkling particle field on a lightweight canvas.\n * Renders a single static frame when reduced motion is preferred.\n */\nexport function Particles({ className, quantity = 60, color }: ParticlesProps) {\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    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    let particles: Particle[] = [];\n    let raf = 0;\n    let width = 0;\n    let height = 0;\n\n    // The canvas inherits `color` from its context, so particles match\n    // the current theme. Computed colors resolve to rgb() in browsers.\n    const resolveColor = () => {\n      if (color) return color;\n      const match = getComputedStyle(canvas).color.match(/\\d+(\\.\\d+)?/g);\n      return match ? match.slice(0, 3).join(\", \") : \"255, 255, 255\";\n    };\n    let rgb = \"255, 255, 255\";\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect();\n      width = rect.width;\n      height = rect.height;\n      canvas.width = width * dpr;\n      canvas.height = height * dpr;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      rgb = resolveColor();\n      particles = Array.from({ length: quantity }, () => ({\n        x: Math.random() * width,\n        y: Math.random() * height,\n        vx: (Math.random() - 0.5) * 0.15,\n        vy: (Math.random() - 0.5) * 0.15,\n        size: Math.random() * 1.6 + 0.4,\n        alpha: Math.random() * 0.5 + 0.1,\n        twinkle: Math.random() * 0.02 + 0.005,\n      }));\n    };\n\n    const draw = () => {\n      ctx.clearRect(0, 0, width, height);\n      for (const p of particles) {\n        ctx.beginPath();\n        ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);\n        ctx.fillStyle = `rgba(${rgb}, ${p.alpha})`;\n        ctx.fill();\n      }\n    };\n\n    const tick = () => {\n      for (const p of particles) {\n        p.x = (p.x + p.vx + width) % width;\n        p.y = (p.y + p.vy + height) % height;\n        p.alpha += p.twinkle;\n        if (p.alpha > 0.6 || p.alpha < 0.1) p.twinkle *= -1;\n      }\n      draw();\n      raf = requestAnimationFrame(tick);\n    };\n\n    resize();\n    if (reduced) {\n      draw();\n    } else {\n      raf = requestAnimationFrame(tick);\n    }\n\n    // Resizing clears the canvas, so redraw the static frame when reduced\n    const resizeObserver = new ResizeObserver(() => {\n      resize();\n      if (reduced) draw();\n    });\n    resizeObserver.observe(canvas);\n\n    // Re-resolve the color when the theme class changes on <html>\n    const themeObserver = new MutationObserver(() => {\n      rgb = resolveColor();\n      if (reduced) draw();\n    });\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    return () => {\n      cancelAnimationFrame(raf);\n      resizeObserver.disconnect();\n      themeObserver.disconnect();\n    };\n  }, [quantity, color]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      aria-hidden\n      data-slot=\"particles\"\n      className={cn(\"pointer-events-none absolute inset-0 size-full\", className)}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/particles.tsx"
    }
  ],
  "type": "registry:ui"
}