{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"magnetic-dot-grid","type":"registry:component","title":"Magnetic Dot Grid","description":"A grid of dots that reacts to the cursor.","meta":{"pro":false},"docs":"Usage: https://atelier-ui.com/r/magnetic-dot-grid.md\nDocs: https://atelier-ui.com/docs/components/cursor/magnetic-dot-grid","dependencies":[],"registryDependencies":["https://atelier-ui.com/r/agent-rules.json"],"files":[{"path":"registry/magnetic-dot-grid/magnetic-dot-grid.tsx","type":"registry:component","target":"components/magnetic-dot-grid/magnetic-dot-grid.tsx","content":"\"use client\"\n\nimport { type ComponentRef, useEffect, useRef } from \"react\"\nimport { useFrameLoop } from \"../../hooks/use-frame-loop\"\n\nconst FALL_OFF = -3.0\n\ntype DotGrid = {\n    baseX: Float32Array\n    baseY: Float32Array\n    currentX: Float32Array\n    currentY: Float32Array\n    seed: Float32Array\n    count: number\n}\n\nexport type MagneticDotGridProps = {\n    dotRadius?: number\n    spacing?: number\n    strength?: number\n    interactionRadius?: number\n    snapSpeed?: number\n    returnSpeed?: number\n    floatAmplitude?: number\n    floatSpeed?: number\n    baseColor?: string\n    centerColors?: string[]\n    cycleSpeed?: number\n    className?: string\n}\n\nfunction hexToRgb(hex: string): [number, number, number] {\n    const v = parseInt(hex.replace(\"#\", \"\"), 16)\n    return [(v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff]\n}\n\nfunction createGrid(width: number, height: number, spacing: number): DotGrid {\n    const cols = Math.floor(width / spacing) + 1\n    const rows = Math.floor(height / spacing) + 1\n    const count = cols * rows\n\n    const offsetX = (width - (cols - 1) * spacing) / 2\n    const offsetY = (height - (rows - 1) * spacing) / 2\n\n    const baseX = new Float32Array(count)\n    const baseY = new Float32Array(count)\n    const currentX = new Float32Array(count)\n    const currentY = new Float32Array(count)\n    const seed = new Float32Array(count)\n\n    let index = 0\n\n    for (let row = 0; row < rows; row++) {\n        for (let col = 0; col < cols; col++) {\n            const x = offsetX + col * spacing\n            const y = offsetY + row * spacing\n            baseX[index] = x\n            baseY[index] = y\n            currentX[index] = x\n            currentY[index] = y\n            seed[index] = Math.random() * 1000\n            index++\n        }\n    }\n\n    return {\n        baseX,\n        baseY,\n        currentX,\n        currentY,\n        count,\n        seed,\n    }\n}\n\nexport function MagneticDotGrid({\n    dotRadius = 0.6,\n    spacing = 16,\n    strength = 20,\n    interactionRadius = 500,\n    snapSpeed = 9,\n    returnSpeed = 5,\n    floatAmplitude = 1.8,\n    floatSpeed = 2,\n    baseColor = \"#000000\",\n    centerColors = [\"#BCBCBC\"],\n    cycleSpeed = 1.5,\n    className,\n}: MagneticDotGridProps) {\n    const canvasRef = useRef<ComponentRef<\"canvas\">>(null)\n    const gridRef = useRef<DotGrid | null>(null)\n    const pointerRef = useRef({ x: 0, y: 0, active: false })\n    const baseColorRef = useRef(hexToRgb(baseColor))\n    const centerColorsRef = useRef(centerColors.map(hexToRgb))\n\n    useEffect(() => {\n        baseColorRef.current = hexToRgb(baseColor)\n        centerColorsRef.current = centerColors.map(hexToRgb)\n    }, [baseColor, centerColors])\n\n    useFrameLoop((time, delta) => {\n        const canvas = canvasRef.current\n        if (!canvas) return\n        const ctx = canvas.getContext(\"2d\")\n        const grid = gridRef.current\n        if (!grid || !ctx) return\n\n        const pointer = pointerRef.current\n        const parsedCenterColors = centerColorsRef.current\n        const parsedBaseColor = baseColorRef.current\n        const progress = ((time * cycleSpeed) % 1) * parsedCenterColors.length\n        const currentColorIndex = Math.floor(progress) % parsedCenterColors.length\n        const nextColorIndex = (currentColorIndex + 1) % parsedCenterColors.length\n        const blend = progress - Math.floor(progress)\n        const currentColor = parsedCenterColors[currentColorIndex]\n        const nextColor = parsedCenterColors[nextColorIndex]\n\n        const centerR = currentColor[0] + (nextColor[0] - currentColor[0]) * blend\n        const centerG = currentColor[1] + (nextColor[1] - currentColor[1]) * blend\n        const centerB = currentColor[2] + (nextColor[2] - currentColor[2]) * blend\n\n        ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight)\n\n        const interactionRadiusSq = interactionRadius * interactionRadius\n\n        for (let i = 0; i < grid.count; i++) {\n            const bx = grid.baseX[i]\n            const by = grid.baseY[i]\n            const seed = grid.seed[i]\n\n            let targetX = bx\n            let targetY = by\n\n            let influence = 0\n\n            if (pointer.active) {\n                const dx = bx - pointer.x\n                const dy = by - pointer.y\n                const distSq = dx * dx + dy * dy\n\n                if (distSq < interactionRadiusSq) {\n                    const dist = Math.sqrt(distSq)\n                    const normalizedDist = dist / interactionRadius\n\n                    influence = Math.exp(FALL_OFF * normalizedDist * normalizedDist)\n\n                    if (dist > 0.001) {\n                        const force = influence * strength\n                        targetX += (dx / dist) * force\n                        targetY += (dy / dist) * force\n                    }\n                }\n            }\n\n            const amp = floatAmplitude * influence\n            const floatX = Math.sin(time * floatSpeed + seed) * amp\n            const floatY = Math.sin(time * floatSpeed * 0.6 + seed * 1.3) * amp\n\n            targetX += floatX\n            targetY += floatY\n\n            const speed = influence > 0.01 ? snapSpeed : returnSpeed\n            const factor = 1 - Math.exp(-speed * delta)\n\n            grid.currentX[i] += (targetX - grid.currentX[i]) * factor\n            grid.currentY[i] += (targetY - grid.currentY[i]) * factor\n\n            const r = parsedBaseColor[0] + (centerR - parsedBaseColor[0]) * influence\n            const g = parsedBaseColor[1] + (centerG - parsedBaseColor[1]) * influence\n            const b = parsedBaseColor[2] + (centerB - parsedBaseColor[2]) * influence\n\n            ctx.fillStyle = `rgb(${r},${g},${b})`\n            ctx.fillRect(\n                grid.currentX[i] - dotRadius,\n                grid.currentY[i] - dotRadius,\n                dotRadius * 2,\n                dotRadius * 2,\n            )\n        }\n    })\n\n    useEffect(() => {\n        const canvas = canvasRef.current\n        if (!canvas) return\n\n        const ctx = canvas.getContext(\"2d\", { alpha: true })\n        if (!ctx) return\n\n        const updatePointer = (clientX: number, clientY: number) => {\n            const canvas = canvasRef.current\n            if (!canvas) return\n            const rect = canvas.getBoundingClientRect()\n            pointerRef.current.x = clientX - rect.left\n            pointerRef.current.y = clientY - rect.top\n            pointerRef.current.active = true\n        }\n\n        const handlePointerMove = (event: PointerEvent) => {\n            updatePointer(event.clientX, event.clientY)\n        }\n\n        const handlePointerLeave = () => {\n            pointerRef.current.active = false\n        }\n\n        const displayGrid = () => {\n            const dpr = window.devicePixelRatio || 1\n            const width = canvas.clientWidth\n            const height = canvas.clientHeight\n\n            canvas.width = width * dpr\n            canvas.height = height * dpr\n            ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n            gridRef.current = createGrid(width, height, spacing)\n        }\n\n        displayGrid()\n        const observer = new ResizeObserver(displayGrid)\n        observer.observe(canvas)\n        canvas.addEventListener(\"pointermove\", handlePointerMove)\n        canvas.addEventListener(\"pointerleave\", handlePointerLeave)\n\n        return () => {\n            observer.disconnect()\n            canvas.removeEventListener(\"pointermove\", handlePointerMove)\n            canvas.removeEventListener(\"pointerleave\", handlePointerLeave)\n        }\n    }, [spacing])\n\n    return (\n        <div className={className}>\n            <canvas ref={canvasRef} style={{ display: \"block\", width: \"100%\", height: \"100%\" }} />\n        </div>\n    )\n}\n"},{"path":"registry/hooks/use-frame-loop.ts","type":"registry:hook","target":"hooks/use-frame-loop.ts","content":"import { useEffect, useRef } from \"react\"\n\nconst DELTA_MAX = 0.1\n\ntype FrameLoopCallback = (time: number, delta: number) => void\n\nexport function useFrameLoop(callback: FrameLoopCallback, interval?: number) {\n    const ref = useRef(callback)\n    ref.current = callback\n\n    useEffect(() => {\n        let frameId = 0\n        let lastTime = 0\n        let lastTick = 0\n\n        const tick = (now: number) => {\n            frameId = requestAnimationFrame(tick)\n\n            if (interval && now - lastTick < interval) return\n            if (interval) lastTick = now\n\n            const time = now * 0.001\n            const delta = lastTime ? Math.min(time - lastTime, DELTA_MAX) : 0\n            lastTime = time\n\n            ref.current(time, delta)\n        }\n\n        frameId = requestAnimationFrame(tick)\n\n        return () => {\n            cancelAnimationFrame(frameId)\n        }\n    }, [interval])\n}\n"}]}