{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kinetic-grid",
  "title": "Kinetic Grid",
  "description": "Kinetic grid animation.",
  "files": [
    {
      "path": "packages/kinetic-grid/kinetic-grid.tsx",
      "content": "\"use client\";\r\n\r\nimport { useEffect, useRef, useCallback, ReactNode } from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\n// ─── Types ────────────────────────────────────────────────────────────────────\r\n\r\ninterface Point {\r\n  x: number;\r\n  y: number;\r\n}\r\n\r\ninterface Ripple {\r\n  x: number;\r\n  y: number;\r\n  radius: number;\r\n  opacity: number;\r\n  born: number;\r\n}\r\n\r\n// ─── Constants ────────────────────────────────────────────────────────────────\r\n\r\nconst CELL_SIZE = 55; // Desktop-ish size. Will dictate cols/rows\r\nconst INFLUENCE_RADIUS = 260;\r\nconst MAX_WARP = 24;\r\nconst DOT_SPACING = 28;\r\nconst LERP_SPEED = 0.08;\r\n\r\nconst LINE_BASE   = { r: 255, g: 255, b: 255, a: 0.13 };\r\nconst NODE_BASE_RADIUS   = 1.8;\r\nconst NODE_ACTIVE_RADIUS = 3.2;\r\n\r\n// ─── Helpers ──────────────────────────────────────────────────────────────────\r\n\r\nfunction lerpN(a: number, b: number, t: number) {\r\n  return a + (b - a) * t;\r\n}\r\n\r\nfunction lerpColor(\r\n  base: { r: number; g: number; b: number; a: number },\r\n  active: { r: number; g: number; b: number; a: number },\r\n  t: number\r\n): string {\r\n  const r = Math.round(lerpN(base.r, active.r, t));\r\n  const g = Math.round(lerpN(base.g, active.g, t));\r\n  const b = Math.round(lerpN(base.b, active.b, t));\r\n  const a = lerpN(base.a, active.a, t);\r\n  return `rgba(${r},${g},${b},${a.toFixed(3)})`;\r\n}\r\n\r\n// ─── Component ────────────────────────────────────────────────────────────────\r\n\r\nexport default function KineticGrid({\r\n  children,\r\n  className,\r\n  globalColor = \"default\",\r\n}: {\r\n  children?: ReactNode;\r\n  className?: string;\r\n  globalColor?: \"default\" | \"monochrome\";\r\n}) {\r\n  const canvasRef = useRef<HTMLCanvasElement>(null);\r\n\r\n  const mouseRef       = useRef<Point>({ x: -9999, y: -9999 });\r\n  const targetMouseRef = useRef<Point>({ x: -9999, y: -9999 });\r\n  const ripplesRef     = useRef<Ripple[]>([]);\r\n  const rafRef         = useRef<number>(0);\r\n  const sizeRef        = useRef<{ w: number; h: number }>({ w: 0, h: 0 });\r\n\r\n  // ── Warp ────────────────────────────────────────────────────────────────────\r\n\r\n  const getWarpedPoint = useCallback(\r\n    (\r\n      gx: number,\r\n      gy: number,\r\n      col: number,\r\n      row: number,\r\n      mouse: Point,\r\n      ripples: Ripple[],\r\n      cols: number,\r\n      rows: number\r\n    ): { pt: Point; proximity: number } => {\r\n      // Edge pin — smoothly locks boundary rows/cols in place\r\n      const edgeMargin = 1.5;\r\n      const colPin = Math.min(col / edgeMargin, (cols - 1 - col) / edgeMargin, 1);\r\n      const rowPin = Math.min(row / edgeMargin, (rows - 1 - row) / edgeMargin, 1);\r\n      const pinFactor = colPin * colPin * rowPin * rowPin;\r\n\r\n      const dx   = gx - mouse.x;\r\n      const dy   = gy - mouse.y;\r\n      const dist = Math.sqrt(dx * dx + dy * dy);\r\n\r\n      const proximity = Math.max(0, 1 - dist / INFLUENCE_RADIUS) * pinFactor;\r\n\r\n      // Ripple displacement\r\n      let rx = 0, ry = 0;\r\n      for (const r of ripples) {\r\n        const rdx   = gx - r.x;\r\n        const rdy   = gy - r.y;\r\n        const rdist = Math.sqrt(rdx * rdx + rdy * rdy);\r\n        const waveWidth = 55;\r\n        const diff = rdist - r.radius;\r\n        if (Math.abs(diff) < waveWidth) {\r\n          const strength = (1 - Math.abs(diff) / waveWidth) * r.opacity * 18 * pinFactor;\r\n          const angle    = Math.atan2(rdy, rdx);\r\n          const sign     = diff < 0 ? -1 : 1;\r\n          rx += Math.cos(angle) * strength * sign * -1;\r\n          ry += Math.sin(angle) * strength * sign * -1;\r\n        }\r\n      }\r\n\r\n      // Cursor warp with bell falloff\r\n      if (dist < INFLUENCE_RADIUS && dist > 0 && pinFactor > 0) {\r\n        const t     = dist / INFLUENCE_RADIUS;\r\n        const eased = t < 0.01 ? 0 : (1 - t) * (1 - t) * Math.min(1, dist / 60);\r\n        const warpAmt = eased * MAX_WARP * pinFactor;\r\n        const angle   = Math.atan2(dy, dx);\r\n        return {\r\n          pt: {\r\n            x: gx - Math.cos(angle) * warpAmt + rx,\r\n            y: gy - Math.sin(angle) * warpAmt + ry,\r\n          },\r\n          proximity,\r\n        };\r\n      }\r\n\r\n      return { pt: { x: gx + rx, y: gy + ry }, proximity };\r\n    },\r\n    []\r\n  );\r\n\r\n  // ── Draw ────────────────────────────────────────────────────────────────────\r\n\r\n  const draw = useCallback(\r\n    (now: number) => {\r\n      const canvas = canvasRef.current;\r\n      if (!canvas) return;\r\n      const ctx = canvas.getContext(\"2d\");\r\n      if (!ctx) return;\r\n\r\n      const { w: W, h: H } = sizeRef.current;\r\n      const mouse   = mouseRef.current;\r\n      const ripples = ripplesRef.current;\r\n\r\n      const theme = {\r\n        default: {\r\n          bg: \"#161618\",\r\n          lineActive: { r: 74,  g: 158, b: 255, a: 0.90 },\r\n          nodeActive: { r: 74,  g: 158, b: 255, a: 1.00 },\r\n          glow: \"74,158,255\",\r\n          ripple: \"100,180,255\",\r\n        },\r\n        monochrome: {\r\n          bg: \"#000000\",\r\n          lineActive: { r: 255, g: 255, b: 255, a: 0.90 },\r\n          nodeActive: { r: 255, g: 255, b: 255, a: 1.00 },\r\n          glow: \"255,255,255\",\r\n          ripple: \"255,255,255\",\r\n        }\r\n      }[globalColor ?? \"default\"];\r\n\r\n      ctx.clearRect(0, 0, W, H);\r\n\r\n      // Background\r\n      ctx.fillStyle = theme.bg;\r\n      ctx.fillRect(0, 0, W, H);\r\n\r\n      // Static background dot texture\r\n      ctx.fillStyle = \"rgba(255,255,255,0.05)\";\r\n      for (let x = DOT_SPACING / 2; x < W; x += DOT_SPACING) {\r\n        for (let y = DOT_SPACING / 2; y < H; y += DOT_SPACING) {\r\n          ctx.beginPath();\r\n          ctx.arc(x, y, 0.7, 0, Math.PI * 2);\r\n          ctx.fill();\r\n        }\r\n      }\r\n\r\n      // Update ripples\r\n      for (let i = ripples.length - 1; i >= 0; i--) {\r\n        const r   = ripples[i];\r\n        const age = (now - r.born) / 1000;\r\n        // FIX: Ensure radius is never negative\r\n        r.radius = Math.max(0, age * 400);\r\n        r.opacity = Math.max(0, 1 - age * 1.2);\r\n        if (r.opacity <= 0) ripples.splice(i, 1);\r\n      }\r\n\r\n      // ── Build warped grid ─────────────────────────────────────────────────\r\n      const cols = Math.max(2, Math.ceil(W / CELL_SIZE)) + 1;\r\n      const rows = Math.max(2, Math.ceil(H / CELL_SIZE)) + 1;\r\n      const cellW = W / (cols - 1);\r\n      const cellH = H / (rows - 1);\r\n      \r\n      const pts: Point[][]   = [];\r\n      const prox: number[][] = [];\r\n\r\n      for (let row = 0; row < rows; row++) {\r\n        pts[row]  = [];\r\n        prox[row] = [];\r\n        for (let col = 0; col < cols; col++) {\r\n          const { pt, proximity } = getWarpedPoint(\r\n            col * cellW, row * cellH, col, row, mouse, ripples, cols, rows\r\n          );\r\n          pts[row][col]  = pt;\r\n          prox[row][col] = proximity;\r\n        }\r\n      }\r\n\r\n      // ── Grid lines ────────────────────────────────────────────────────────\r\n      const drawSeg = (p1: Point, p2: Point, pr1: number, pr2: number) => {\r\n        const avg = (pr1 + pr2) / 2;\r\n        const t   = avg * avg * (3 - 2 * avg); // smoothstep\r\n        ctx.beginPath();\r\n        ctx.moveTo(p1.x, p1.y);\r\n        ctx.lineTo(p2.x, p2.y);\r\n        ctx.strokeStyle = lerpColor(LINE_BASE, theme.lineActive, t);\r\n        ctx.lineWidth   = lerpN(0.8, 1.5, t);\r\n        ctx.stroke();\r\n      };\r\n\r\n      ctx.lineCap = \"butt\";\r\n\r\n      for (let row = 0; row < rows; row++)\r\n        for (let col = 0; col < cols - 1; col++)\r\n          drawSeg(pts[row][col], pts[row][col + 1], prox[row][col], prox[row][col + 1]);\r\n\r\n      for (let col = 0; col < cols; col++)\r\n        for (let row = 0; row < rows - 1; row++)\r\n          drawSeg(pts[row][col], pts[row + 1][col], prox[row][col], prox[row + 1][col]);\r\n\r\n      // ── Intersection nodes ────────────────────────────────────────────────\r\n      for (let row = 0; row < rows; row++) {\r\n        for (let col = 0; col < cols; col++) {\r\n          const p  = pts[row][col];\r\n          const pr = prox[row][col];\r\n          const t  = pr * pr * (3 - 2 * pr); // smoothstep\r\n          const r  = lerpN(NODE_BASE_RADIUS, NODE_ACTIVE_RADIUS, t);\r\n\r\n          // Outer glow ring for active nodes\r\n          if (t > 0.3) {\r\n            const glowR = r + lerpN(0, 6, (t - 0.3) / 0.7);\r\n            const grd = ctx.createRadialGradient(p.x, p.y, r * 0.5, p.x, p.y, glowR);\r\n            grd.addColorStop(0, `rgba(${theme.glow},${(t * 0.30).toFixed(3)})`);\r\n            grd.addColorStop(1, `rgba(${theme.glow},0)`);\r\n            ctx.beginPath();\r\n            ctx.arc(p.x, p.y, glowR, 0, Math.PI * 2);\r\n            ctx.fillStyle = grd;\r\n            ctx.fill();\r\n          }\r\n\r\n          // Node fill\r\n          ctx.beginPath();\r\n          ctx.arc(p.x, p.y, r, 0, Math.PI * 2);\r\n          ctx.fillStyle = lerpColor(\r\n            { r: 255, g: 255, b: 255, a: 0.20 },\r\n            theme.nodeActive,\r\n            t\r\n          );\r\n          ctx.fill();\r\n        }\r\n      }\r\n\r\n      // ── Ripple rings ──────────────────────────────────────────────────────\r\n      for (const r of ripples) {\r\n        // FIX: Ensure radius is positive before drawing arc\r\n        const safeRadius = Math.max(0, r.radius);\r\n        ctx.beginPath();\r\n        ctx.arc(r.x, r.y, safeRadius, 0, Math.PI * 2);\r\n        ctx.strokeStyle = `rgba(${theme.ripple},${(r.opacity * 0.28).toFixed(3)})`;\r\n        ctx.lineWidth   = 1.5;\r\n        ctx.stroke();\r\n      }\r\n    },\r\n    [getWarpedPoint, globalColor]\r\n  );\r\n\r\n  // ── Animation loop ──────────────────────────────────────────────────────────\r\n\r\n  const animate = useCallback(\r\n    (now: number) => {\r\n      const m = mouseRef.current;\r\n      const t = targetMouseRef.current;\r\n\r\n      m.x = lerpN(m.x, t.x, LERP_SPEED);\r\n      m.y = lerpN(m.y, t.y, LERP_SPEED);\r\n\r\n      draw(now);\r\n      rafRef.current = requestAnimationFrame(animate);\r\n    },\r\n    [draw]\r\n  );\r\n\r\n  // ── Setup ───────────────────────────────────────────────────────────────────\r\n\r\n  useEffect(() => {\r\n    const canvas = canvasRef.current;\r\n    if (!canvas) return;\r\n\r\n    const setSize = () => {\r\n      const w = window.innerWidth;\r\n      const h = window.innerHeight;\r\n      canvas.width  = w;\r\n      canvas.height = h;\r\n      sizeRef.current = { w, h };\r\n      if (mouseRef.current.x === -9999) {\r\n        mouseRef.current       = { x: -9999, y: -9999 };\r\n        targetMouseRef.current = { x: -9999, y: -9999 };\r\n      }\r\n    };\r\n\r\n    setSize();\r\n    window.addEventListener(\"resize\", setSize);\r\n\r\n    const onMouseMove = (e: MouseEvent) => {\r\n      targetMouseRef.current = { x: e.clientX, y: e.clientY };\r\n    };\r\n\r\n    const onClick = (e: MouseEvent) => {\r\n      ripplesRef.current.push({\r\n        x: e.clientX, y: e.clientY,\r\n        radius: 0, opacity: 1,\r\n        born: performance.now(),\r\n      });\r\n    };\r\n\r\n    window.addEventListener(\"mousemove\", onMouseMove);\r\n    window.addEventListener(\"click\", onClick);\r\n    rafRef.current = requestAnimationFrame(animate);\r\n\r\n    return () => {\r\n      window.removeEventListener(\"resize\", setSize);\r\n      window.removeEventListener(\"mousemove\", onMouseMove);\r\n      window.removeEventListener(\"click\", onClick);\r\n      if (rafRef.current) {\r\n        cancelAnimationFrame(rafRef.current);\r\n      }\r\n    };\r\n  }, [animate]);\r\n\r\n  // ── Render ──────────────────────────────────────────────────────────────────\r\n\r\n  return (\r\n    <div className={cn(\"relative w-full min-h-screen overflow-hidden\", globalColor === \"monochrome\" ? \"bg-[#000000]\" : \"bg-[#161618]\", className)}>\r\n      <canvas ref={canvasRef} className=\"fixed inset-0 w-full h-full z-0 pointer-events-none\" />\r\n\r\n      <div className=\"relative z-10 w-full h-full\">\r\n        {children}\r\n      </div>\r\n    </div>\r\n  );\r\n}",
      "type": "registry:component",
      "target": "components/satori-ui/kinetic-grid.tsx"
    }
  ],
  "type": "registry:component"
}