{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "DotGrid-JS-TW",
	"title": "DotGrid",
	"description": "Animated dot grid with cursor interactions.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "DotGrid/DotGrid.jsx",
			"content": "'use client';\nimport { useRef, useEffect, useCallback, useMemo } from 'react';\nimport { gsap } from 'gsap';\nimport { InertiaPlugin } from 'gsap/InertiaPlugin';\n\ngsap.registerPlugin(InertiaPlugin);\n\nconst throttle = (func, limit) => {\n  let lastCall = 0;\n  return function (...args) {\n    const now = performance.now();\n    if (now - lastCall >= limit) {\n      lastCall = now;\n      func.apply(this, args);\n    }\n  };\n};\n\nfunction hexToRgb(hex) {\n  const m = hex.match(/^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i);\n  if (!m) return { r: 0, g: 0, b: 0 };\n  return {\n    r: parseInt(m[1], 16),\n    g: parseInt(m[2], 16),\n    b: parseInt(m[3], 16)\n  };\n}\n\nconst DotGrid = ({\n  dotSize = 16,\n  gap = 32,\n  baseColor = '#5227FF',\n  activeColor = '#5227FF',\n  proximity = 150,\n  speedTrigger = 100,\n  shockRadius = 250,\n  shockStrength = 5,\n  maxSpeed = 5000,\n  resistance = 750,\n  returnDuration = 1.5,\n  className = '',\n  style\n}) => {\n  const wrapperRef = useRef(null);\n  const canvasRef = useRef(null);\n  const dotsRef = useRef([]);\n  const pointerRef = useRef({\n    x: 0,\n    y: 0,\n    vx: 0,\n    vy: 0,\n    speed: 0,\n    lastTime: 0,\n    lastX: 0,\n    lastY: 0\n  });\n\n  const baseRgb = useMemo(() => hexToRgb(baseColor), [baseColor]);\n  const activeRgb = useMemo(() => hexToRgb(activeColor), [activeColor]);\n\n  const circlePath = useMemo(() => {\n    if (typeof window === 'undefined' || !window.Path2D) return null;\n\n    const p = new Path2D();\n    p.arc(0, 0, dotSize / 2, 0, Math.PI * 2);\n    return p;\n  }, [dotSize]);\n\n  const buildGrid = useCallback(() => {\n    const wrap = wrapperRef.current;\n    const canvas = canvasRef.current;\n    if (!wrap || !canvas) return;\n\n    const { width, height } = wrap.getBoundingClientRect();\n    const dpr = window.devicePixelRatio || 1;\n\n    canvas.width = width * dpr;\n    canvas.height = height * dpr;\n    canvas.style.width = `${width}px`;\n    canvas.style.height = `${height}px`;\n    const ctx = canvas.getContext('2d');\n    if (ctx) ctx.scale(dpr, dpr);\n\n    const cols = Math.floor((width + gap) / (dotSize + gap));\n    const rows = Math.floor((height + gap) / (dotSize + gap));\n    const cell = dotSize + gap;\n\n    const gridW = cell * cols - gap;\n    const gridH = cell * rows - gap;\n\n    const extraX = width - gridW;\n    const extraY = height - gridH;\n\n    const startX = extraX / 2 + dotSize / 2;\n    const startY = extraY / 2 + dotSize / 2;\n\n    const dots = [];\n    for (let y = 0; y < rows; y++) {\n      for (let x = 0; x < cols; x++) {\n        const cx = startX + x * cell;\n        const cy = startY + y * cell;\n        dots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false });\n      }\n    }\n    dotsRef.current = dots;\n  }, [dotSize, gap]);\n\n  useEffect(() => {\n    if (!circlePath) return;\n\n    let rafId;\n    const proxSq = proximity * proximity;\n\n    const draw = () => {\n      const canvas = canvasRef.current;\n      if (!canvas) return;\n      const ctx = canvas.getContext('2d');\n      if (!ctx) return;\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n      const { x: px, y: py } = pointerRef.current;\n\n      for (const dot of dotsRef.current) {\n        const ox = dot.cx + dot.xOffset;\n        const oy = dot.cy + dot.yOffset;\n        const dx = dot.cx - px;\n        const dy = dot.cy - py;\n        const dsq = dx * dx + dy * dy;\n\n        let style = baseColor;\n        if (dsq <= proxSq) {\n          const dist = Math.sqrt(dsq);\n          const t = 1 - dist / proximity;\n          const r = Math.round(baseRgb.r + (activeRgb.r - baseRgb.r) * t);\n          const g = Math.round(baseRgb.g + (activeRgb.g - baseRgb.g) * t);\n          const b = Math.round(baseRgb.b + (activeRgb.b - baseRgb.b) * t);\n          style = `rgb(${r},${g},${b})`;\n        }\n\n        ctx.save();\n        ctx.translate(ox, oy);\n        ctx.fillStyle = style;\n        ctx.fill(circlePath);\n        ctx.restore();\n      }\n\n      rafId = requestAnimationFrame(draw);\n    };\n\n    draw();\n    return () => cancelAnimationFrame(rafId);\n  }, [proximity, baseColor, activeRgb, baseRgb, circlePath]);\n\n  useEffect(() => {\n    buildGrid();\n    let ro = null;\n    if ('ResizeObserver' in window) {\n      ro = new ResizeObserver(buildGrid);\n      wrapperRef.current && ro.observe(wrapperRef.current);\n    } else {\n      window.addEventListener('resize', buildGrid);\n    }\n    return () => {\n      if (ro) ro.disconnect();\n      else window.removeEventListener('resize', buildGrid);\n    };\n  }, [buildGrid]);\n\n  useEffect(() => {\n    const onMove = e => {\n      const now = performance.now();\n      const pr = pointerRef.current;\n      const dt = pr.lastTime ? now - pr.lastTime : 16;\n      const dx = e.clientX - pr.lastX;\n      const dy = e.clientY - pr.lastY;\n      let vx = (dx / dt) * 1000;\n      let vy = (dy / dt) * 1000;\n      let speed = Math.hypot(vx, vy);\n      if (speed > maxSpeed) {\n        const scale = maxSpeed / speed;\n        vx *= scale;\n        vy *= scale;\n        speed = maxSpeed;\n      }\n      pr.lastTime = now;\n      pr.lastX = e.clientX;\n      pr.lastY = e.clientY;\n      pr.vx = vx;\n      pr.vy = vy;\n      pr.speed = speed;\n\n      const rect = canvasRef.current.getBoundingClientRect();\n      pr.x = e.clientX - rect.left;\n      pr.y = e.clientY - rect.top;\n\n      for (const dot of dotsRef.current) {\n        const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y);\n        if (speed > speedTrigger && dist < proximity && !dot._inertiaApplied) {\n          dot._inertiaApplied = true;\n          gsap.killTweensOf(dot);\n          const pushX = dot.cx - pr.x + vx * 0.005;\n          const pushY = dot.cy - pr.y + vy * 0.005;\n          gsap.to(dot, {\n            inertia: { xOffset: pushX, yOffset: pushY, resistance },\n            onComplete: () => {\n              gsap.to(dot, {\n                xOffset: 0,\n                yOffset: 0,\n                duration: returnDuration,\n                ease: 'elastic.out(1,0.75)'\n              });\n              dot._inertiaApplied = false;\n            }\n          });\n        }\n      }\n    };\n\n    const onClick = e => {\n      const rect = canvasRef.current.getBoundingClientRect();\n      const cx = e.clientX - rect.left;\n      const cy = e.clientY - rect.top;\n      for (const dot of dotsRef.current) {\n        const dist = Math.hypot(dot.cx - cx, dot.cy - cy);\n        if (dist < shockRadius && !dot._inertiaApplied) {\n          dot._inertiaApplied = true;\n          gsap.killTweensOf(dot);\n          const falloff = Math.max(0, 1 - dist / shockRadius);\n          const pushX = (dot.cx - cx) * shockStrength * falloff;\n          const pushY = (dot.cy - cy) * shockStrength * falloff;\n          gsap.to(dot, {\n            inertia: { xOffset: pushX, yOffset: pushY, resistance },\n            onComplete: () => {\n              gsap.to(dot, {\n                xOffset: 0,\n                yOffset: 0,\n                duration: returnDuration,\n                ease: 'elastic.out(1,0.75)'\n              });\n              dot._inertiaApplied = false;\n            }\n          });\n        }\n      }\n    };\n\n    const throttledMove = throttle(onMove, 50);\n    window.addEventListener('mousemove', throttledMove, { passive: true });\n    window.addEventListener('click', onClick);\n\n    return () => {\n      window.removeEventListener('mousemove', throttledMove);\n      window.removeEventListener('click', onClick);\n    };\n  }, [maxSpeed, speedTrigger, proximity, resistance, returnDuration, shockRadius, shockStrength]);\n\n  return (\n    <section className={`p-4 flex items-center justify-center h-full w-full relative ${className}`} style={style}>\n      <div ref={wrapperRef} className=\"w-full h-full relative\">\n        <canvas ref={canvasRef} className=\"absolute inset-0 w-full h-full pointer-events-none\" />\n      </div>\n    </section>\n  );\n};\n\nexport default DotGrid;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0"
	]
}