{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dotted-glow-background",
  "title": "Dotted Glow Background",
  "description": "Canvas dot grid where drifting glows brighten dots and tint them toward the brand.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/dotted-glow-background.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface DottedGlowBackgroundProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Distance between dot centres in px */\n  gap?: number;\n  /** Dot radius in px (lit dots grow up to 1.6×) */\n  radius?: number;\n  /** Opacity of unlit dots, 0–1 */\n  opacity?: number;\n  /** Glow travel speed multiplier */\n  speed?: number;\n  /** Number of travelling glows */\n  glows?: number;\n  /** Pause while the pointer is over the parent element (focus inside it always pauses) */\n  pauseOnHover?: boolean;\n}\n\nconst LEVELS = 16;\n\n// Resolves any CSS colour (oklch tokens included) to \"r,g,b\" via a 1×1 canvas.\nfunction rgb(color: string) {\n  const c = document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true })!;\n  c.fillStyle = color;\n  c.fillRect(0, 0, 1, 1);\n  return [...c.getImageData(0, 0, 1, 1).data.slice(0, 3)];\n}\n\n/**\n * A canvas dot grid with soft glows drifting across it; dots near a glow\n * brighten, grow and tint toward `--brand`. Unlit dots use the inherited text\n * colour. Absolutely positioned — place inside a `relative overflow-hidden`\n * element.\n */\nexport function DottedGlowBackground({\n  gap = 18,\n  radius = 1.2,\n  opacity = 0.25,\n  speed = 1,\n  glows = 3,\n  pauseOnHover = true,\n  className,\n  ...props\n}: DottedGlowBackgroundProps) {\n  const ref = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const el = ref.current!;\n    const ctx = el.getContext(\"2d\");\n    const host = el.parentElement?.parentElement;\n    if (!ctx || !host) return;\n    const mq = matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let w = 0;\n    let h = 0;\n    let t = 0;\n    let last = 0;\n    let raf = 0;\n    let seen = false;\n    let hover = false;\n    let focus = false;\n    let tint = \"0,0,0\";\n    let styles: string[] = [];\n\n    const colors = () => {\n      const cs = getComputedStyle(el);\n      const a = rgb(cs.color);\n      const b = rgb(cs.getPropertyValue(\"--brand\") || cs.color);\n      tint = b.join();\n      styles = Array.from({ length: LEVELS + 1 }, (_, i) => {\n        const k = i / LEVELS;\n        return `rgba(${a.map((v, j) => Math.round(v + (b[j] - v) * k))},${opacity + (1 - opacity) * k})`;\n      });\n    };\n\n    const draw = () => {\n      ctx.clearRect(0, 0, w, h);\n      const s = (t / 1000) * speed * 0.15;\n      const R = Math.max(w, h) * 0.13 + 30;\n      const lights = Array.from({ length: glows }, (_, i) => {\n        const x = w * (0.5 + 0.45 * Math.sin(s * (0.9 + i * 0.37) + i * 2.1));\n        const y = h * (0.5 + 0.45 * Math.cos(s * (0.7 + i * 0.23) + i * 1.3));\n        // A faint brand halo under each glow\n        const g = ctx.createRadialGradient(x, y, 0, x, y, R * 1.5);\n        g.addColorStop(0, `rgba(${tint},.1)`);\n        g.addColorStop(1, `rgba(${tint},0)`);\n        ctx.fillStyle = g;\n        ctx.fillRect(0, 0, w, h);\n        return [x, y];\n      });\n      // Batch dots into one path per brightness level: a few fills per frame.\n      const paths = styles.map(() => new Path2D());\n      const inv = 1 / (2 * R * R);\n      const d = Math.max(gap, 4);\n      for (let x = (w % d) / 2 + d / 2; x < w; x += d) {\n        for (let y = (h % d) / 2 + d / 2; y < h; y += d) {\n          let v = 0;\n          for (const [lx, ly] of lights) v += Math.exp(-((x - lx) ** 2 + (y - ly) ** 2) * inv);\n          const i = Math.round(Math.min(v, 1) * LEVELS);\n          const r = radius * (1 + (0.6 * i) / LEVELS);\n          paths[i].moveTo(x + r, y);\n          paths[i].arc(x, y, r, 0, 7);\n        }\n      }\n      paths.forEach((p, i) => {\n        ctx.fillStyle = styles[i];\n        ctx.fill(p);\n      });\n    };\n\n    const frame = (now: number) => {\n      t += Math.min(now - last, 50);\n      last = now;\n      draw();\n      raf = requestAnimationFrame(frame);\n    };\n    // Loop only on screen, in a visible tab, without reduced motion and while\n    // nobody is hovering or focused inside; otherwise hold a still frame.\n    const sync = () => {\n      cancelAnimationFrame(raf);\n      if (mq.matches || !seen || document.hidden || focus || (hover && pauseOnHover)) draw();\n      else {\n        last = performance.now();\n        raf = requestAnimationFrame(frame);\n      }\n    };\n    const resize = () => {\n      const dpr = Math.min(devicePixelRatio || 1, 2);\n      w = el.clientWidth;\n      h = el.clientHeight;\n      el.width = w * dpr;\n      el.height = h * dpr;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      sync();\n    };\n    const on = (e: Event) => {\n      if (e.type == \"pointerenter\") hover = true;\n      else if (e.type == \"pointerleave\") hover = false;\n      else focus = host.contains((e as FocusEvent).relatedTarget as Node) || e.type == \"focusin\";\n      sync();\n    };\n    const events = [\"pointerenter\", \"pointerleave\", \"focusin\", \"focusout\"];\n\n    colors();\n    const ro = new ResizeObserver(resize);\n    const io = new IntersectionObserver(([e]) => {\n      seen = e.isIntersecting;\n      sync();\n    });\n    const mo = new MutationObserver(() => {\n      colors();\n      sync();\n    });\n    ro.observe(el);\n    io.observe(el);\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"style\"] });\n    events.forEach((n) => host.addEventListener(n, on));\n    document.addEventListener(\"visibilitychange\", sync);\n    mq.addEventListener(\"change\", sync);\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      events.forEach((n) => host.removeEventListener(n, on));\n      document.removeEventListener(\"visibilitychange\", sync);\n      mq.removeEventListener(\"change\", sync);\n    };\n  }, [gap, radius, opacity, speed, glows, pauseOnHover]);\n\n  return (\n    <div\n      aria-hidden\n      data-slot=\"dotted-glow-background\"\n      className={cn(\"pointer-events-none absolute inset-0 overflow-hidden\", className)}\n      {...props}\n    >\n      <canvas ref={ref} className=\"absolute inset-0 size-full\" />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/dotted-glow-background.tsx"
    }
  ],
  "type": "registry:ui"
}