{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "CursorGrid-TS-TW",
	"title": "CursorGrid",
	"description": "Canvas grid whose cells light up around the cursor with configurable radius, falloff and click pulses.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "CursorGrid/CursorGrid.tsx",
			"content": "import { useRef, useEffect } from 'react';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface CursorGridProps {\n  cellSize?: number;\n  color?: string;\n  radius?: number;\n  falloff?: Falloff;\n  holdTime?: number;\n  fadeDuration?: number;\n  lineWidth?: number;\n  maxOpacity?: number;\n  fillOpacity?: number;\n  gridOpacity?: number;\n  cellRadius?: number;\n  clickPulse?: boolean;\n  pulseSpeed?: number;\n  className?: string;\n}\n\ninterface GridConfig {\n  cellSize: number;\n  color: string;\n  radius: number;\n  falloff: Falloff;\n  holdTime: number;\n  fadeDuration: number;\n  lineWidth: number;\n  maxOpacity: number;\n  fillOpacity: number;\n  gridOpacity: number;\n  cellRadius: number;\n  clickPulse: boolean;\n  pulseSpeed: number;\n}\n\ninterface Pulse {\n  x: number;\n  y: number;\n  t0: number;\n}\n\nconst FALLOFF_CURVES: Record<Falloff, (t: number) => number> = {\n  linear: t => t,\n  smooth: t => t * t * (3 - 2 * t),\n  sharp: t => t * t * t\n};\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n  const h = hex.replace('#', '');\n  const v = h.length === 3 ? h.split('').map(c => c + c).join('') : h;\n  const num = parseInt(v.slice(0, 6), 16);\n  return [(num >> 16) & 255, (num >> 8) & 255, num & 255];\n};\n\nconst CursorGrid = ({\n  cellSize = 70,\n  color = '#D946EF',\n  radius = 140,\n  falloff = 'smooth',\n  holdTime = 400,\n  fadeDuration = 800,\n  lineWidth = 1.2,\n  maxOpacity = 1,\n  fillOpacity = 0,\n  gridOpacity = 0,\n  cellRadius = 0,\n  clickPulse = true,\n  pulseSpeed = 600,\n  className = ''\n}: CursorGridProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const propsRef = useRef<GridConfig>({} as GridConfig);\n  const wakeRef = useRef<(() => void) | null>(null);\n\n  propsRef.current = {\n    cellSize,\n    color,\n    radius,\n    falloff,\n    holdTime,\n    fadeDuration,\n    lineWidth,\n    maxOpacity,\n    fillOpacity,\n    gridOpacity,\n    cellRadius,\n    clickPulse,\n    pulseSpeed\n  };\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    if (!container || !canvas) return;\n\n    const ctx = canvas.getContext('2d');\n    if (!ctx) return;\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n\n    // Grid state: one alpha + timestamp pair per cell, indexed row-major.\n    let cols = 0;\n    let rows = 0;\n    let offX = 0;\n    let offY = 0;\n    let alphas = new Float32Array(0);\n    let touched = new Float64Array(0);\n    let w = 0;\n    let h = 0;\n    const pulses: Pulse[] = [];\n    let raf = 0;\n    let running = false;\n    let lastFrame = 0;\n\n    const rebuild = () => {\n      const p = propsRef.current;\n      w = container.offsetWidth;\n      h = container.offsetHeight;\n      canvas.width = Math.max(1, Math.round(w * dpr));\n      canvas.height = Math.max(1, Math.round(h * dpr));\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      cols = Math.ceil(w / p.cellSize) + 1;\n      rows = Math.ceil(h / p.cellSize) + 1;\n      // Center the lattice so edge cells crop evenly on both sides\n      offX = (w - cols * p.cellSize) / 2;\n      offY = (h - rows * p.cellSize) / 2;\n      alphas = new Float32Array(cols * rows);\n      touched = new Float64Array(cols * rows);\n    };\n\n    const cellCenter = (i: number): [number, number] => {\n      const p = propsRef.current;\n      const cx = offX + (i % cols) * p.cellSize + p.cellSize / 2;\n      const cy = offY + Math.floor(i / cols) * p.cellSize + p.cellSize / 2;\n      return [cx, cy];\n    };\n\n    // Light up every cell whose center falls inside the radius, with the\n    // configured falloff curve mapping distance to brightness.\n    const energize = (x: number, y: number, boost?: number) => {\n      const p = propsRef.current;\n      const r = Math.max(p.radius, 1);\n      const ease = FALLOFF_CURVES[p.falloff] ?? FALLOFF_CURVES.linear;\n      const now = performance.now();\n      const minCol = Math.max(0, Math.floor((x - r - offX) / p.cellSize));\n      const maxCol = Math.min(cols - 1, Math.floor((x + r - offX) / p.cellSize));\n      const minRow = Math.max(0, Math.floor((y - r - offY) / p.cellSize));\n      const maxRow = Math.min(rows - 1, Math.floor((y + r - offY) / p.cellSize));\n      for (let cRow = minRow; cRow <= maxRow; cRow++) {\n        for (let cCol = minCol; cCol <= maxCol; cCol++) {\n          const i = cRow * cols + cCol;\n          const [cx, cy] = cellCenter(i);\n          const dist = Math.hypot(cx - x, cy - y);\n          if (dist > r) continue;\n          const level = ease(1 - dist / r) * p.maxOpacity * (boost ?? 1);\n          if (level > alphas[i]) {\n            alphas[i] = level;\n            touched[i] = now;\n          } else if (level > 0) {\n            touched[i] = now;\n          }\n        }\n      }\n    };\n\n    const draw = (now: number) => {\n      const p = propsRef.current;\n      const dt = Math.min(now - lastFrame, 50);\n      lastFrame = now;\n      ctx.clearRect(0, 0, w, h);\n      const [cr, cg, cb] = hexToRgb(p.color);\n\n      // Optional faint static lattice\n      if (p.gridOpacity > 0) {\n        ctx.strokeStyle = `rgba(${cr}, ${cg}, ${cb}, ${p.gridOpacity})`;\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        for (let cCol = 0; cCol <= cols; cCol++) {\n          const x = Math.round(offX + cCol * p.cellSize) + 0.5;\n          ctx.moveTo(x, 0);\n          ctx.lineTo(x, h);\n        }\n        for (let cRow = 0; cRow <= rows; cRow++) {\n          const y = Math.round(offY + cRow * p.cellSize) + 0.5;\n          ctx.moveTo(0, y);\n          ctx.lineTo(w, y);\n        }\n        ctx.stroke();\n      }\n\n      // Expanding click pulses hand their energy to cells as they pass\n      for (let pi = pulses.length - 1; pi >= 0; pi--) {\n        const pulse = pulses[pi];\n        const age = (now - pulse.t0) / 1000;\n        const ringR = age * p.pulseSpeed;\n        if (ringR > Math.hypot(w, h)) {\n          pulses.splice(pi, 1);\n          continue;\n        }\n        const band = p.cellSize;\n        const minCol = Math.max(0, Math.floor((pulse.x - ringR - band - offX) / p.cellSize));\n        const maxCol = Math.min(cols - 1, Math.floor((pulse.x + ringR + band - offX) / p.cellSize));\n        const minRow = Math.max(0, Math.floor((pulse.y - ringR - band - offY) / p.cellSize));\n        const maxRow = Math.min(rows - 1, Math.floor((pulse.y + ringR + band - offY) / p.cellSize));\n        for (let cRow = minRow; cRow <= maxRow; cRow++) {\n          for (let cCol = minCol; cCol <= maxCol; cCol++) {\n            const i = cRow * cols + cCol;\n            const [cx, cy] = cellCenter(i);\n            const dist = Math.hypot(cx - pulse.x, cy - pulse.y);\n            if (Math.abs(dist - ringR) < band / 2 && p.maxOpacity > alphas[i]) {\n              alphas[i] = p.maxOpacity;\n              touched[i] = now;\n            }\n          }\n        }\n      }\n\n      let anyVisible = pulses.length > 0;\n      const fadeStep = dt / Math.max(p.fadeDuration, 16);\n      const half = p.cellSize / 2;\n\n      for (let i = 0; i < alphas.length; i++) {\n        let a = alphas[i];\n        if (a <= 0) continue;\n        if (now - touched[i] > p.holdTime) {\n          a = Math.max(0, a - fadeStep);\n          alphas[i] = a;\n          if (a <= 0) continue;\n        }\n        anyVisible = true;\n\n        const [cx, cy] = cellCenter(i);\n        const gradient = ctx.createRadialGradient(cx, cy, half * 0.1, cx, cy, p.cellSize);\n        gradient.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, ${a})`);\n        gradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);\n\n        const x = cx - half + 0.5;\n        const y = cy - half + 0.5;\n        const s = p.cellSize - 1;\n\n        ctx.beginPath();\n        if (p.cellRadius > 0) {\n          ctx.roundRect(x, y, s, s, p.cellRadius);\n        } else {\n          ctx.rect(x, y, s, s);\n        }\n        if (p.fillOpacity > 0) {\n          ctx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, ${a * p.fillOpacity})`;\n          ctx.fill();\n        }\n        ctx.strokeStyle = gradient;\n        ctx.lineWidth = p.lineWidth;\n        ctx.stroke();\n      }\n\n      if (anyVisible) {\n        raf = requestAnimationFrame(draw);\n      } else {\n        running = false;\n        if (propsRef.current.gridOpacity <= 0) ctx.clearRect(0, 0, w, h);\n      }\n    };\n\n    const wake = () => {\n      if (running) return;\n      running = true;\n      lastFrame = performance.now();\n      raf = requestAnimationFrame(draw);\n    };\n    wakeRef.current = wake;\n\n    const toLocal = (e: PointerEvent): [number, number] => {\n      const rect = canvas.getBoundingClientRect();\n      return [e.clientX - rect.left, e.clientY - rect.top];\n    };\n\n    const onPointerMove = (e: PointerEvent) => {\n      const [x, y] = toLocal(e);\n      energize(x, y);\n      wake();\n    };\n\n    const onPointerDown = (e: PointerEvent) => {\n      if (!propsRef.current.clickPulse) return;\n      const [x, y] = toLocal(e);\n      pulses.push({ x, y, t0: performance.now() });\n      wake();\n    };\n\n    const ro = new ResizeObserver(() => {\n      rebuild();\n      wake();\n    });\n    ro.observe(container);\n    rebuild();\n    wake();\n\n    container.addEventListener('pointermove', onPointerMove);\n    container.addEventListener('pointerdown', onPointerDown);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      container.removeEventListener('pointermove', onPointerMove);\n      container.removeEventListener('pointerdown', onPointerDown);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [cellSize]);\n\n  // Repaint static layers when visual props change while idle\n  useEffect(() => {\n    wakeRef.current?.();\n  }, [gridOpacity, color, lineWidth, maxOpacity, fillOpacity, cellRadius]);\n\n  return (\n    <div ref={containerRef} className={`relative h-full w-full overflow-hidden${className ? ` ${className}` : ''}`}>\n      <canvas ref={canvasRef} className=\"block h-full w-full\" />\n    </div>\n  );\n};\n\nexport default CursorGrid;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}