{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-field",
  "title": "Dot Field",
  "description": "An interactive dot-grid background that reacts to the cursor.",
  "dependencies": [],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/dot-field.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { motionScale } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype DotFieldProps = React.ComponentProps<\"div\"> & {\n  /** Distance between dots in px. */\n  gap?: number;\n  /** Radius (px) around the pointer where dots react. */\n  radius?: number;\n  /** Clicking sends a ripple across the field. */\n  ripple?: boolean;\n};\n\n/**\n * An interactive dot grid background on canvas: dots near the pointer swell and take the signal color, and a click\n * sends a ripple across the field. Theme-aware (reads tokens), DPR-sharp, paused when off screen; static with motion off.\n */\nfunction DotField({ className, gap = 22, radius = 120, ripple = true, children, ...props }: DotFieldProps) {\n  const ref = React.useRef<HTMLDivElement>(null);\n\n  React.useEffect(() => {\n    const host = ref.current;\n    const canvas = host?.querySelector(\"canvas\");\n    const ctx = canvas?.getContext(\"2d\");\n    if (!host || !canvas || !ctx) return;\n    let w = 0;\n    let h = 0;\n    let frame = 0;\n    let visible = true;\n    const pointer = { x: -9999, y: -9999 };\n    const waves: { x: number; y: number; t: number }[] = [];\n    const animated = motionScale(host) > 0;\n    const color = (name: string) => getComputedStyle(host).getPropertyValue(name).trim() || \"#888\";\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      w = host.clientWidth;\n      h = host.clientHeight;\n      canvas.width = w * dpr;\n      canvas.height = h * dpr;\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    };\n\n    const draw = (time: number) => {\n      ctx.clearRect(0, 0, w, h);\n      const base = color(\"--cl-mark\");\n      const signal = color(\"--signal\");\n      for (let x = gap / 2; x < w; x += gap) {\n        for (let y = gap / 2; y < h; y += gap) {\n          let k = 0;\n          const d = Math.hypot(x - pointer.x, y - pointer.y);\n          if (d < radius) k = 1 - d / radius;\n          for (const wv of waves) {\n            const r = (time - wv.t) * 0.45;\n            const dd = Math.abs(Math.hypot(x - wv.x, y - wv.y) - r);\n            if (dd < 30) k = Math.max(k, (1 - dd / 30) * Math.max(0, 1 - r / Math.max(w, h)));\n          }\n          ctx.globalAlpha = 0.35 + k * 0.65;\n          ctx.fillStyle = k > 0.15 ? signal : base;\n          ctx.beginPath();\n          ctx.arc(x, y, 1 + k * 2.2, 0, Math.PI * 2);\n          ctx.fill();\n        }\n      }\n      ctx.globalAlpha = 1;\n      while (waves.length && time - waves[0]!.t > 4000) waves.shift();\n      if (animated && visible) frame = requestAnimationFrame(draw);\n    };\n\n    const onMove = (e: PointerEvent) => {\n      const r = host.getBoundingClientRect();\n      pointer.x = e.clientX - r.left;\n      pointer.y = e.clientY - r.top;\n    };\n    const onLeave = () => {\n      pointer.x = pointer.y = -9999;\n    };\n    const onDown = (e: PointerEvent) => {\n      if (!ripple) return;\n      const r = host.getBoundingClientRect();\n      waves.push({ x: e.clientX - r.left, y: e.clientY - r.top, t: performance.now() });\n    };\n\n    resize();\n    const ro = new ResizeObserver(() => {\n      resize();\n      if (!animated) draw(0);\n    });\n    ro.observe(host);\n    const io = new IntersectionObserver(([entry]) => {\n      visible = !!entry?.isIntersecting;\n      cancelAnimationFrame(frame);\n      if (visible && animated) frame = requestAnimationFrame(draw);\n    });\n    io.observe(host);\n    host.addEventListener(\"pointermove\", onMove);\n    host.addEventListener(\"pointerleave\", onLeave);\n    host.addEventListener(\"pointerdown\", onDown);\n    draw(performance.now());\n    return () => {\n      cancelAnimationFrame(frame);\n      ro.disconnect();\n      io.disconnect();\n      host.removeEventListener(\"pointermove\", onMove);\n      host.removeEventListener(\"pointerleave\", onLeave);\n      host.removeEventListener(\"pointerdown\", onDown);\n    };\n  }, [gap, radius, ripple]);\n\n  return (\n    <div ref={ref} data-slot=\"dot-field\" className={cn(\"relative overflow-hidden\", className)} {...props}>\n      <canvas aria-hidden=\"true\" className=\"absolute inset-0\" />\n      <div className=\"relative\">{children}</div>\n    </div>\n  );\n}\n\nexport { DotField };\nexport type { DotFieldProps };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/dot-field",
  "categories": [
    "effects"
  ],
  "type": "registry:ui"
}