{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "DotField-JS-CSS",
	"title": "DotField",
	"description": "Interactive dot grid with cursor bulge, glow, sparkle, and wave effects.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "DotField.css",
			"target": "@components/DotField.css",
			"content": ".dot-field-container {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "DotField.jsx",
			"content": "import { useEffect, useRef, memo } from 'react';\n\nimport './DotField.css';\n\nconst TWO_PI = Math.PI * 2;\n\nconst DotField = memo(({\n  dotRadius = 1.5,\n  dotSpacing = 14,\n  cursorRadius = 500,\n  cursorForce = 0.1,\n  bulgeOnly = true,\n  bulgeStrength = 67,\n  glowRadius = 160,\n  sparkle = false,\n  waveAmplitude = 0,\n  gradientFrom = 'rgba(168, 85, 247, 0.35)',\n  gradientTo = 'rgba(180, 151, 207, 0.25)',\n  glowColor = '#120F17',\n  ...rest\n}) => {\n  const canvasRef = useRef(null);\n  const svgRef = useRef(null);\n  const glowRef = useRef(null);\n  const dotsRef = useRef([]);\n  const mouseRef = useRef({ x: -9999, y: -9999, prevX: -9999, prevY: -9999, speed: 0 });\n  const rafRef = useRef(null);\n  const sizeRef = useRef({ w: 0, h: 0, offsetX: 0, offsetY: 0 });\n  const glowOpacity = useRef(0);\n  const engagement = useRef(0);\n  const propsRef = useRef({});\n  propsRef.current = { dotRadius, dotSpacing, cursorRadius, cursorForce, bulgeOnly, bulgeStrength, sparkle, waveAmplitude, gradientFrom, gradientTo };\n  const rebuildRef = useRef(null);\n  const glowIdRef = useRef(`dot-field-glow-${Math.random().toString(36).slice(2, 9)}`);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const glowEl = glowRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext('2d', { alpha: true });\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    let resizeTimer;\n\n    function resize() {\n      clearTimeout(resizeTimer);\n      resizeTimer = setTimeout(doResize, 100);\n    }\n\n    function doResize() {\n      const rect = canvas.parentElement.getBoundingClientRect();\n      const w = rect.width;\n      const h = rect.height;\n\n      canvas.width = w * dpr;\n      canvas.height = h * dpr;\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n      sizeRef.current = {\n        w,\n        h,\n        offsetX: rect.left + window.scrollX,\n        offsetY: rect.top + window.scrollY,\n      };\n\n      buildDots(w, h);\n    }\n\n    function buildDots(w, h) {\n      const p = propsRef.current;\n      const step = p.dotRadius + p.dotSpacing;\n      const cols = Math.floor(w / step);\n      const rows = Math.floor(h / step);\n      const padX = (w % step) / 2;\n      const padY = (h % step) / 2;\n      const dots = new Array(rows * cols);\n      let idx = 0;\n\n      for (let row = 0; row < rows; row++) {\n        for (let col = 0; col < cols; col++) {\n          const ax = padX + col * step + step / 2;\n          const ay = padY + row * step + step / 2;\n          dots[idx++] = { ax, ay, sx: ax, sy: ay, vx: 0, vy: 0, x: ax, y: ay };\n        }\n      }\n      dotsRef.current = dots;\n    }\n\n    function onMouseMove(e) {\n      const s = sizeRef.current;\n      mouseRef.current.x = e.pageX - s.offsetX;\n      mouseRef.current.y = e.pageY - s.offsetY;\n    }\n\n    function updateMouseSpeed() {\n      const m = mouseRef.current;\n      const dx = m.prevX - m.x;\n      const dy = m.prevY - m.y;\n      const dist = Math.sqrt(dx * dx + dy * dy);\n      m.speed += (dist - m.speed) * 0.5;\n      if (m.speed < 0.001) m.speed = 0;\n      m.prevX = m.x;\n      m.prevY = m.y;\n    }\n\n    const speedInterval = setInterval(updateMouseSpeed, 20);\n\n    let frameCount = 0;\n\n    function tick() {\n      frameCount++;\n      const dots = dotsRef.current;\n      const m = mouseRef.current;\n      const { w, h } = sizeRef.current;\n      const p = propsRef.current;\n      const len = dots.length;\n      const t = frameCount * 0.02;\n\n      const targetEngagement = Math.min(m.speed / 5, 1);\n      engagement.current += (targetEngagement - engagement.current) * 0.06;\n      if (engagement.current < 0.001) engagement.current = 0;\n      const eng = engagement.current;\n\n      glowOpacity.current += (eng - glowOpacity.current) * 0.08;\n\n      if (glowEl) {\n        glowEl.setAttribute('cx', m.x);\n        glowEl.setAttribute('cy', m.y);\n        glowEl.style.opacity = glowOpacity.current;\n      }\n\n      ctx.clearRect(0, 0, w, h);\n\n      const grad = ctx.createLinearGradient(0, 0, w, h);\n      grad.addColorStop(0, p.gradientFrom);\n      grad.addColorStop(1, p.gradientTo);\n      ctx.fillStyle = grad;\n\n      const cr = p.cursorRadius;\n      const crSq = cr * cr;\n      const rad = p.dotRadius / 2;\n      const isBulge = p.bulgeOnly;\n\n      ctx.beginPath();\n\n      for (let i = 0; i < len; i++) {\n        const d = dots[i];\n        const dx = m.x - d.ax;\n        const dy = m.y - d.ay;\n        const distSq = dx * dx + dy * dy;\n\n        if (distSq < crSq && eng > 0.01) {\n          const dist = Math.sqrt(distSq);\n          if (isBulge) {\n            const t = 1 - dist / cr;\n            const push = t * t * p.bulgeStrength * eng;\n            const angle = Math.atan2(dy, dx);\n            d.sx += (d.ax - Math.cos(angle) * push - d.sx) * 0.15;\n            d.sy += (d.ay - Math.sin(angle) * push - d.sy) * 0.15;\n          } else {\n            const angle = Math.atan2(dy, dx);\n            const move = (500 / dist) * (m.speed * p.cursorForce);\n            d.vx += Math.cos(angle) * -move;\n            d.vy += Math.sin(angle) * -move;\n          }\n        } else if (isBulge) {\n          d.sx += (d.ax - d.sx) * 0.1;\n          d.sy += (d.ay - d.sy) * 0.1;\n        }\n\n        if (!isBulge) {\n          d.vx *= 0.9;\n          d.vy *= 0.9;\n          d.x = d.ax + d.vx;\n          d.y = d.ay + d.vy;\n          d.sx += (d.x - d.sx) * 0.1;\n          d.sy += (d.y - d.sy) * 0.1;\n        }\n\n        let drawX = d.sx;\n        let drawY = d.sy;\n        if (p.waveAmplitude > 0) {\n          drawY += Math.sin(d.ax * 0.03 + t) * p.waveAmplitude;\n          drawX += Math.cos(d.ay * 0.03 + t * 0.7) * p.waveAmplitude * 0.5;\n        }\n\n        if (p.sparkle) {\n          const hash = ((i * 2654435761) ^ (frameCount >> 3)) >>> 0;\n          if ((hash % 100) < 3) {\n            ctx.moveTo(drawX + rad * 1.8, drawY);\n            ctx.arc(drawX, drawY, rad * 1.8, 0, TWO_PI);\n          } else {\n            ctx.moveTo(drawX + rad, drawY);\n            ctx.arc(drawX, drawY, rad, 0, TWO_PI);\n          }\n        } else {\n          ctx.moveTo(drawX + rad, drawY);\n          ctx.arc(drawX, drawY, rad, 0, TWO_PI);\n        }\n      }\n\n      ctx.fill();\n\n      rafRef.current = requestAnimationFrame(tick);\n    }\n\n    doResize();\n    window.addEventListener('resize', resize);\n    window.addEventListener('mousemove', onMouseMove, { passive: true });\n    rafRef.current = requestAnimationFrame(tick);\n\n    rebuildRef.current = () => {\n      const { w, h } = sizeRef.current;\n      if (w > 0 && h > 0) buildDots(w, h);\n    };\n\n    return () => {\n      cancelAnimationFrame(rafRef.current);\n      clearInterval(speedInterval);\n      clearTimeout(resizeTimer);\n      window.removeEventListener('resize', resize);\n      window.removeEventListener('mousemove', onMouseMove);\n    };\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  useEffect(() => {\n    rebuildRef.current?.();\n  }, [dotRadius, dotSpacing]);\n\n  return (\n    <div className=\"dot-field-container\" {...rest}>\n      <canvas\n        ref={canvasRef}\n        style={{\n          position: 'absolute',\n          inset: 0,\n          width: '100%',\n          height: '100%',\n        }}\n      />\n      <svg\n        ref={svgRef}\n        style={{\n          position: 'absolute',\n          inset: 0,\n          width: '100%',\n          height: '100%',\n          pointerEvents: 'none',\n        }}\n      >\n        <defs>\n          <radialGradient id={glowIdRef.current}>\n            <stop offset=\"0%\" stopColor={glowColor} />\n            <stop offset=\"100%\" stopColor=\"transparent\" />\n          </radialGradient>\n        </defs>\n        <circle\n          ref={glowRef}\n          cx=\"-9999\"\n          cy=\"-9999\"\n          r={glowRadius}\n          fill={`url(#${glowIdRef.current})`}\n          style={{ opacity: 0, willChange: 'opacity' }}\n        />\n      </svg>\n    </div>\n  );\n});\n\nDotField.displayName = 'DotField';\n\nexport default DotField;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}