{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dotgrid",
  "type": "registry:component",
  "title": "Dot Grid",
  "description": "Full-viewport canvas dot grid that drifts toward the cursor; pauses when the tab is hidden, off under reduced motion.",
  "dependencies": [],
  "registryDependencies": [
    "https://mocoui.site/r/tokens.json",
    "https://mocoui.site/r/hooks.json"
  ],
  "files": [
    {
      "path": "registry/dotgrid/dotgrid.tsx",
      "type": "registry:component",
      "target": "components/moco/dotgrid.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useReducedMotion } from \"@/components/moco/hooks\";\nimport \"./dotgrid.css\";\n\nexport interface DotGridProps {\n  /** Grid pitch in px. */\n  spacing?: number;\n  /** Cursor influence radius in px. */\n  radius?: number;\n}\n\n/**\n * Sparse dot grid that reacts to the cursor. Pauses when the tab is hidden and\n * does not run at all under prefers-reduced-motion. Dot color is resolved from\n * the canvas's computed `color` (set to var(--moco-ink) in dotgrid.css), so it\n * stays visible in dark mode; only the alpha ramp is applied on top.\n */\nexport function DotGrid({ spacing = 34, radius = 130 }: DotGridProps) {\n  const ref = React.useRef<HTMLCanvasElement>(null);\n  const reduced = useReducedMotion();\n\n  React.useEffect(() => {\n    if (reduced) return;\n    const canvas = ref.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!canvas || !ctx) return;\n\n    let w = 0;\n    let h = 0;\n    let dpr = 1;\n    const pointer = { x: -9999, y: -9999 };\n    let raf = 0;\n    let running = true;\n\n    // Base dot color, re-resolved on color-scheme change and resize.\n    let rgb = \"0,0,0\";\n    const resolveColor = () => {\n      const m = getComputedStyle(canvas).color.match(/[\\d.]+/g);\n      if (m && m.length >= 3) rgb = `${m[0]},${m[1]},${m[2]}`;\n    };\n\n    const resize = () => {\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      w = canvas.clientWidth;\n      h = canvas.clientHeight;\n      canvas.width = Math.floor(w * dpr);\n      canvas.height = Math.floor(h * dpr);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      resolveColor();\n    };\n\n    const draw = () => {\n      ctx.clearRect(0, 0, w, h);\n      for (let x = spacing / 2; x < w; x += spacing) {\n        for (let y = spacing / 2; y < h; y += spacing) {\n          const dx = x - pointer.x;\n          const dy = y - pointer.y;\n          const d = Math.hypot(dx, dy);\n          const k = d < radius ? 1 - d / radius : 0;\n          const alpha = 0.07 + k * 0.26;\n          const push = k * 4;\n          const px = x + (d ? (dx / d) * push : 0);\n          const py = y + (d ? (dy / d) * push : 0);\n          ctx.fillStyle = `rgba(${rgb},${alpha.toFixed(3)})`;\n          ctx.fillRect(px, py, 1, 1);\n        }\n      }\n      if (running) raf = requestAnimationFrame(draw);\n    };\n\n    const onMove = (e: PointerEvent) => {\n      pointer.x = e.clientX;\n      pointer.y = e.clientY;\n    };\n    const onLeave = () => {\n      pointer.x = -9999;\n      pointer.y = -9999;\n    };\n    const onVis = () => {\n      running = !document.hidden;\n      if (running) raf = requestAnimationFrame(draw);\n      else cancelAnimationFrame(raf);\n    };\n\n    const scheme = window.matchMedia(\"(prefers-color-scheme: dark)\");\n    scheme.addEventListener(\"change\", resolveColor);\n\n    resize();\n    raf = requestAnimationFrame(draw);\n    window.addEventListener(\"resize\", resize);\n    window.addEventListener(\"pointermove\", onMove, { passive: true });\n    window.addEventListener(\"pointerleave\", onLeave);\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    return () => {\n      running = false;\n      cancelAnimationFrame(raf);\n      scheme.removeEventListener(\"change\", resolveColor);\n      window.removeEventListener(\"resize\", resize);\n      window.removeEventListener(\"pointermove\", onMove);\n      window.removeEventListener(\"pointerleave\", onLeave);\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [spacing, radius, reduced]);\n\n  return <canvas ref={ref} className=\"moco-dotgrid\" aria-hidden=\"true\" />;\n}\n"
    },
    {
      "path": "registry/dotgrid/dotgrid.css",
      "type": "registry:file",
      "target": "components/moco/dotgrid.css",
      "content": ".moco-dotgrid {\n  position: fixed;\n  inset: 0;\n  z-index: 0;\n  pointer-events: none;\n  /* Not painted by CSS — the canvas reads its computed `color` at runtime\n     for dot fill, so dots follow the ink token in light and dark. */\n  color: var(--moco-ink);\n}\n"
    }
  ],
  "docs": "import { DotGrid } from \"./dotgrid\";\n\nexport default function Demo() {\n  return (\n    <div style={{ minHeight: \"60vh\" }}>\n      <DotGrid />\n      <p style={{ position: \"relative\", zIndex: 1, padding: \"2rem\" }}>\n        Move the cursor — nearby dots brighten and drift toward it.\n      </p>\n    </div>\n  );\n}\n",
  "meta": {
    "tags": [
      "canvas",
      "background",
      "motion",
      "decoration"
    ]
  }
}