{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-grid-spotlight",
  "title": "Dot Grid Spotlight",
  "author": "ncdai <dai@chanhdai.com>",
  "description": "Interactive dot grid with a cursor-tracking spotlight effect.",
  "files": [
    {
      "path": "src/registry/components/dot-grid-spotlight/dot-grid-spotlight.tsx",
      "content": "\"use client\"\n\nimport React, { useEffect, useRef } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type DotGridSpotlightProps = {\n  /**\n   * The base color of the default/inactive dots.\n   * @default \"rgba(255, 255, 255, 0.05)\"\n   */\n  dotColor?: string\n\n  /**\n   * The color of the active dots when illuminated by the cursor's spotlight.\n   * @default \"rgba(255, 255, 255, 0.1)\"\n   */\n  activeDotColor?: string\n\n  /**\n   * The distance (in pixels) between each dot in the grid.\n   * @default 10\n   */\n  spacing?: number\n\n  /**\n   * The default radius of the dots when they are outside the interaction area.\n   * @default 1\n   */\n  baseRadius?: number\n\n  /**\n   * The maximum radius of a dot when it is at the exact center of the cursor.\n   * @default 2\n   */\n  activeRadius?: number\n\n  /**\n   * The radius (in pixels) of the interaction area (spotlight) around the cursor.\n   * @default 128\n   */\n  interactionRadius?: number\n\n  /**\n   * The maximum opacity (alpha) at the exact center of the spotlight.\n   * Accepts a value between `0` and `1` (e.g., `1` for full opacity).\n   * @default 1.0\n   */\n  activeMaxAlpha?: number\n\n  /**\n   * The minimum opacity (alpha) at the outer edge of the spotlight.\n   * Accepts a value between `0` and `1` (e.g., a low value for a soft, subtle fade).\n   * @default 0.5\n   */\n  activeMinAlpha?: number\n\n  /**\n   * Optional CSS class name to apply to the canvas or its wrapper.\n   */\n  className?: string\n}\n\nexport function DotGridSpotlight({\n  dotColor = \"rgba(255, 255, 255, 0.05)\",\n  activeDotColor = \"rgba(255, 255, 255, 0.1)\",\n  spacing = 10,\n  baseRadius = 1,\n  activeRadius = 2,\n  interactionRadius = 128,\n  activeMaxAlpha = 1.0,\n  activeMinAlpha = 0.5,\n  className,\n}: DotGridSpotlightProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const mouse = useRef({ x: -1000, y: -1000, isActive: false })\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas) return\n\n    const ctx = canvas.getContext(\"2d\")\n    if (!ctx) return\n\n    let width = 0\n    let height = 0\n    let renderFrameId: number | null = null\n\n    const draw = () => {\n      ctx.clearRect(0, 0, width, height)\n\n      const offsetX = (width % spacing) / 2\n      const offsetY = (height % spacing) / 2\n\n      for (let x = offsetX; x <= width; x += spacing) {\n        for (let y = offsetY; y <= height; y += spacing) {\n          const dx = x - mouse.current.x\n          const dy = y - mouse.current.y\n          const distance = Math.sqrt(dx * dx + dy * dy)\n\n          let currentRadius = baseRadius\n          let currentColor = dotColor\n          let currentAlpha = 1.0\n\n          if (mouse.current.isActive && distance < interactionRadius) {\n            const factor = 1 - distance / interactionRadius\n            currentRadius = baseRadius + (activeRadius - baseRadius) * factor\n            currentColor = activeDotColor\n            currentAlpha =\n              activeMinAlpha + (activeMaxAlpha - activeMinAlpha) * factor\n          }\n\n          ctx.globalAlpha = currentAlpha\n          ctx.beginPath()\n          ctx.arc(x, y, currentRadius, 0, Math.PI * 2)\n          ctx.fillStyle = currentColor\n          ctx.fill()\n        }\n      }\n      ctx.globalAlpha = 1.0\n    }\n\n    const resizeCanvas = () => {\n      const parent = canvas.parentElement\n      if (!parent) return\n\n      const dpr = window.devicePixelRatio || 1\n      width = parent.clientWidth\n      height = parent.clientHeight\n\n      if (width === 0 || height === 0) return\n\n      canvas.width = width * dpr\n      canvas.height = height * dpr\n      canvas.style.width = `${width}px`\n      canvas.style.height = `${height}px`\n      ctx.scale(dpr, dpr)\n\n      draw()\n\n      requestAnimationFrame(() => {\n        canvas.dataset.ready = \"true\"\n      })\n    }\n\n    const handleMouseMove = (e: MouseEvent) => {\n      const rect = canvas.getBoundingClientRect()\n      mouse.current = {\n        x: e.clientX - rect.left,\n        y: e.clientY - rect.top,\n        isActive: true,\n      }\n\n      if (renderFrameId === null) {\n        renderFrameId = requestAnimationFrame(() => {\n          draw()\n          renderFrameId = null\n        })\n      }\n    }\n\n    const handleMouseLeave = () => {\n      mouse.current.isActive = false\n      if (renderFrameId === null) {\n        renderFrameId = requestAnimationFrame(() => {\n          draw()\n          renderFrameId = null\n        })\n      }\n    }\n\n    canvas.addEventListener(\"mousemove\", handleMouseMove)\n    canvas.addEventListener(\"mouseleave\", handleMouseLeave)\n\n    const resizeObserver = new ResizeObserver(() => resizeCanvas())\n    if (canvas.parentElement) resizeObserver.observe(canvas.parentElement)\n\n    resizeCanvas()\n\n    return () => {\n      canvas.removeEventListener(\"mousemove\", handleMouseMove)\n      canvas.removeEventListener(\"mouseleave\", handleMouseLeave)\n      resizeObserver.disconnect()\n      if (renderFrameId !== null) cancelAnimationFrame(renderFrameId)\n    }\n  }, [\n    spacing,\n    baseRadius,\n    activeRadius,\n    interactionRadius,\n    dotColor,\n    activeDotColor,\n    activeMaxAlpha,\n    activeMinAlpha,\n  ])\n\n  const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {\n    const rect = canvasRef.current?.getBoundingClientRect()\n    if (rect) {\n      mouse.current = {\n        x: e.clientX - rect.left,\n        y: e.clientY - rect.top,\n        isActive: true,\n      }\n    }\n  }\n\n  const handleMouseLeave = () => {\n    mouse.current.isActive = false\n  }\n\n  return (\n    <canvas\n      ref={canvasRef}\n      data-ready=\"false\"\n      className={cn(\n        \"pointer-events-auto absolute inset-0 block opacity-0 transition-opacity! duration-500 data-[ready=true]:opacity-100\",\n        className\n      )}\n      onMouseMove={handleMouseMove}\n      onMouseLeave={handleMouseLeave}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/dot-grid-spotlight.tsx"
    }
  ],
  "docs": "https://chanhdai.com/components/dot-grid-spotlight",
  "categories": [
    "effects"
  ],
  "type": "registry:component"
}