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