{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "particles",
  "type": "registry:component",
  "title": "Particles",
  "description": "A canvas particle field that drifts behind your content and parts around the pointer, or gathers toward it.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://ui.minidev.pro/r/button.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/premium/particles.tsx",
      "type": "registry:ui",
      "target": "components/ui/particles.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport { ArrowRight } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\n/**\n * A canvas particle field that drifts behind your content and parts around the pointer, or gathers toward it.\n * Colour comes from the accent token and follows theme and material changes.\n * The canvas is sharp on high density screens, resizes with its box, sleeps\n * off screen and in background tabs, and draws one still frame for reduced\n * motion.\n */\ntype ParticlesProps = {\n  /** Number of particles. */\n  quantity?: number\n  /** Radius of the nearest particles in pixels. Farther ones are smaller. */\n  size?: number\n  /** Any CSS colour, including var(--token). Defaults to the accent. */\n  color?: string\n  /** Push particles away from the pointer, or pull them toward it. */\n  mode?: \"repel\" | \"attract\"\n  /** Reach of the pointer in pixels. */\n  radius?: number\n  /** Drift speed multiplier. 0 holds the field still apart from the pointer. */\n  drift?: number\n  className?: string\n  children?: React.ReactNode\n}\n\ntype Dot = { x: number; y: number; z: number; vx: number; vy: number; ox: number; oy: number; a: number; ph: number }\n\nfunction Particles({\n  quantity = 110,\n  size = 2,\n  color = \"var(--accent)\",\n  mode = \"repel\",\n  radius = 120,\n  drift = 1,\n  className,\n  children,\n}: ParticlesProps) {\n  const reduce = useReducedMotion()\n  const boxRef = React.useRef<HTMLDivElement>(null)\n  const canvasRef = React.useRef<HTMLCanvasElement>(null)\n\n  React.useEffect(() => {\n    const box = boxRef.current\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext(\"2d\")\n    if (!box || !canvas || !ctx) return\n\n    let w = 0\n    let h = 0\n    let dpr = 1\n    let raf = 0\n    let last = 0\n    let visible = true\n    let fill = \"\"\n    const pointer = { x: 0, y: 0, on: false }\n    const dots: Dot[] = []\n\n    // Resolve the colour through the cascade, so var(--accent) follows theme and material.\n    const readColor = () => {\n      canvas.style.color = color\n      fill = getComputedStyle(canvas).color || \"currentColor\"\n    }\n\n    const seed = () => {\n      dots.length = 0\n      for (let i = 0; i < Math.max(0, Math.round(quantity)); i++) {\n        const z = [0.45, 0.7, 1][i % 3]\n        const dir = Math.random() * Math.PI * 2\n        const v = (4 + Math.random() * 8) * z\n        dots.push({\n          x: Math.random() * w,\n          y: Math.random() * h,\n          z,\n          vx: Math.cos(dir) * v,\n          vy: Math.sin(dir) * v - 3 * z,\n          ox: 0,\n          oy: 0,\n          a: 0.25 + 0.55 * z * (0.6 + Math.random() * 0.4),\n          ph: Math.random() * Math.PI * 2,\n        })\n      }\n    }\n\n    const resize = () => {\n      const r = box.getBoundingClientRect()\n      const nw = Math.max(1, Math.round(r.width))\n      const nh = Math.max(1, Math.round(r.height))\n      dpr = Math.min(window.devicePixelRatio || 1, 2)\n      canvas.width = Math.round(nw * dpr)\n      canvas.height = Math.round(nh * dpr)\n      if (!dots.length || !w || !h) {\n        w = nw\n        h = nh\n        seed()\n      } else {\n        // Keep each particle's relative place, so a resize never reshuffles the field.\n        for (const d of dots) {\n          d.x = (d.x / w) * nw\n          d.y = (d.y / h) * nh\n        }\n        w = nw\n        h = nh\n      }\n    }\n\n    const draw = (t: number, dt: number) => {\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n      ctx.clearRect(0, 0, w, h)\n      ctx.fillStyle = fill\n      const pull = mode === \"attract\"\n      const m = 12\n      for (const d of dots) {\n        if (dt > 0) {\n          d.x += d.vx * drift * dt\n          d.y += d.vy * drift * dt\n          if (d.x < -m) d.x += w + m * 2\n          else if (d.x > w + m) d.x -= w + m * 2\n          if (d.y < -m) d.y += h + m * 2\n          else if (d.y > h + m) d.y -= h + m * 2\n\n          let tx = 0\n          let ty = 0\n          if (pointer.on) {\n            const dx = d.x - pointer.x\n            const dy = d.y - pointer.y\n            const dist = Math.hypot(dx, dy) || 1\n            if (dist < radius) {\n              const k = 1 - dist / radius\n              const ease = k * k * (3 - 2 * k)\n              if (pull) {\n                tx = -dx * ease * 0.55\n                ty = -dy * ease * 0.55\n              } else {\n                tx = (dx / dist) * ease * radius * 0.5 * d.z\n                ty = (dy / dist) * ease * radius * 0.5 * d.z\n              }\n            }\n          }\n          // Ease toward the pointer's target offset; settles back when the pointer leaves.\n          const follow = 1 - Math.pow(0.0025, dt)\n          d.ox += (tx - d.ox) * follow\n          d.oy += (ty - d.oy) * follow\n        }\n        const twinkle = 0.75 + 0.25 * Math.sin(t * 0.0012 + d.ph)\n        ctx.globalAlpha = d.a * twinkle\n        ctx.beginPath()\n        ctx.arc(d.x + d.ox, d.y + d.oy, Math.max(0.4, size * d.z), 0, Math.PI * 2)\n        ctx.fill()\n      }\n      ctx.globalAlpha = 1\n    }\n\n    const loop = (t: number) => {\n      const dt = last ? Math.min(0.05, (t - last) / 1000) : 0\n      last = t\n      draw(t, dt)\n      raf = requestAnimationFrame(loop)\n    }\n    const stop = () => {\n      cancelAnimationFrame(raf)\n      raf = 0\n      last = 0\n    }\n    const sync = () => {\n      if (reduce) {\n        stop()\n        draw(0, 0)\n        return\n      }\n      const run = visible && !document.hidden\n      if (run && !raf) raf = requestAnimationFrame(loop)\n      if (!run && raf) stop()\n    }\n\n    const onMove = (e: PointerEvent) => {\n      const r = canvas.getBoundingClientRect()\n      const x = e.clientX - r.left\n      const y = e.clientY - r.top\n      pointer.on = x >= -radius && y >= -radius && x <= r.width + radius && y <= r.height + radius\n      pointer.x = x\n      pointer.y = y\n    }\n    const onLeave = () => {\n      pointer.on = false\n    }\n\n    readColor()\n    resize()\n    const ro = new ResizeObserver(() => {\n      resize()\n      if (reduce || !raf) draw(0, 0)\n    })\n    const io = new IntersectionObserver(([e]) => {\n      visible = e.isIntersecting\n      sync()\n    })\n    const mo = new MutationObserver(() => {\n      readColor()\n      if (reduce || !raf) draw(0, 0)\n    })\n    ro.observe(box)\n    io.observe(box)\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"data-material\"] })\n    document.addEventListener(\"visibilitychange\", sync)\n    if (!reduce) {\n      window.addEventListener(\"pointermove\", onMove, { passive: true })\n      document.documentElement.addEventListener(\"pointerleave\", onLeave)\n    }\n    sync()\n\n    return () => {\n      stop()\n      ro.disconnect()\n      io.disconnect()\n      mo.disconnect()\n      document.removeEventListener(\"visibilitychange\", sync)\n      window.removeEventListener(\"pointermove\", onMove)\n      document.documentElement.removeEventListener(\"pointerleave\", onLeave)\n    }\n  }, [quantity, size, color, mode, radius, drift, reduce])\n\n  return (\n    <div\n      ref={boxRef}\n      data-slot=\"particles\"\n      className={cn(\"relative isolate h-[360px] w-full overflow-hidden rounded-2xl border border-border bg-bg\", className)}\n    >\n      <canvas ref={canvasRef} aria-hidden data-slot=\"particles-canvas\" className=\"pointer-events-none absolute inset-0 z-0 size-full\" />\n      <div className=\"relative z-10 h-full\">\n        {children ?? (\n          <div className=\"flex h-full flex-col items-center justify-center gap-4 px-6 text-center\">\n            <span className=\"rounded-full border border-accent-line bg-accent-soft px-2.5 py-0.5 text-xs font-medium text-accent-fg\">\n              Lumen for teams\n            </span>\n            <p className=\"max-w-[16ch] text-3xl font-medium tracking-[-0.035em] text-balance text-fg\">\n              Every invoice, accounted for\n            </p>\n            <Button>\n              Start free trial\n              <ArrowRight />\n            </Button>\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { Particles }\nexport type { ParticlesProps }\n"
    }
  ]
}
