{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "DotField-JS-TW", "title": "DotField", "description": "Interactive dot grid with cursor bulge, glow, sparkle, and wave effects.", "type": "registry:component", "files": [ { "type": "registry:component", "path": "DotField/DotField.jsx", "content": "import { useEffect, useRef, memo } from 'react';\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
\n \n \n \n \n \n \n \n \n \n \n
\n );\n});\n\nDotField.displayName = 'DotField';\n\nexport default DotField;\n" } ], "registryDependencies": [], "dependencies": [] }