{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "weight-grid",
  "title": "Weight Grid",
  "description": "A bento grid where the cell under the cursor takes area from its neighbours and the total never changes. Track weights are renormalised against their axis sum, so growing a cell is taking space off the ones beside it — not growing the grid.",
  "dependencies": [],
  "files": [
    {
      "path": "src/components/WeightGrid/index.js",
      "content": "export { default } from './WeightGrid.jsx'\n",
      "type": "registry:component",
      "target": "components/weight-grid/index.js"
    },
    {
      "path": "src/components/WeightGrid/WeightGrid.jsx",
      "content": "'use client'\n\nimport { useEffect, useMemo, useRef } from 'react'\nimport './WeightGrid.css'\n\n/* =========================================================\n   WeightGrid\n   A bento grid where the cell under the cursor takes area\n   from its neighbours and the total never changes.\n\n   The trick is PressureText's, moved into two dimensions.\n   Each row and column has a weight; the cursor raises the\n   weights it is near, and then every weight is divided by\n   the sum of its axis:\n\n     wᵢ ← wᵢ / Σw   ·   n\n\n   Because the fractions are renormalised, the tracks always\n   sum to the same total — so the grid never changes size and\n   nothing outside it moves. Growing a cell is not growing\n   the grid; it is taking space off the cells beside it.\n\n   Written as `fr` values on grid-template-rows/columns, which\n   means the browser does the layout and the cells keep real\n   flow content. A transform-based version would be cheaper\n   per frame but would scale the text inside each cell, and a\n   bento grid whose type warps under the cursor reads as a\n   bug rather than as weight.\n   ========================================================= */\n\nconst clampN = (n, lo, hi) => (n < lo ? lo : n > hi ? hi : n)\n\nconst FALLOFF = {\n  linear: (t) => t,\n  smooth: (t) => t * t * (3 - 2 * t),\n  sharp: (t) => t * t,\n}\n\nexport default function WeightGrid({\n  columns = 4,\n  rows = 3,\n  gain = 1.1,          // how much extra weight the axis nearest the cursor gets\n  cursorRadius = 1.2,  // reach, in tracks\n  smoothing = 0.075,   // per-frame approach rate\n  falloff = 'smooth',\n  gap = 10,\n  children,\n  className = '',\n  style,\n  ...rest\n}) {\n  const rootRef = useRef(null)\n\n  const live = useRef({})\n  live.current = { columns, rows, gain, cursorRadius, smoothing, falloff }\n\n  const kids = useMemo(() => children, [children])\n\n  useEffect(() => {\n    const root = rootRef.current\n    if (!root) return\n\n    const still = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n    const hoverable = window.matchMedia('(hover: hover)').matches\n\n    let cw = new Float32Array(columns).fill(1)\n    let rw = new Float32Array(rows).fill(1)\n    let tx = -1e5   // cursor, in fractional track coordinates\n    let ty = -1e5\n    let raf = 0\n    let onScreen = true\n\n    /* Weights for one axis: a bump around the cursor, then renormalised so the\n       axis still sums to n. The renormalisation is the whole component. */\n    const axis = (out, n, at, want) => {\n      const shape = FALLOFF[live.current.falloff] || FALLOFF.smooth\n      const { gain: g, cursorRadius: r, smoothing: s } = live.current\n      let sum = 0\n      for (let i = 0; i < n; i++) {\n        const d = Math.abs(i + 0.5 - at) / Math.max(0.05, r)\n        want[i] = 1 + g * shape(clampN(1 - d, 0, 1))\n        sum += want[i]\n      }\n      // Divide through: the axis sums to exactly n however hard it is pushed.\n      const k = n / (sum || 1)\n      let moving = false\n      for (let i = 0; i < n; i++) {\n        const target = want[i] * k\n        const next = out[i] + (target - out[i]) * s\n        if (Math.abs(next - out[i]) > 0.0005) moving = true\n        out[i] = next\n      }\n      return moving\n    }\n\n    const wantC = new Float32Array(64)\n    const wantR = new Float32Array(64)\n\n    const paint = () => {\n      root.style.gridTemplateColumns = Array.from(cw, (v) => `${v.toFixed(4)}fr`).join(' ')\n      root.style.gridTemplateRows = Array.from(rw, (v) => `${v.toFixed(4)}fr`).join(' ')\n    }\n\n    const frame = () => {\n      const a = axis(cw, cw.length, tx, wantC)\n      const b = axis(rw, rw.length, ty, wantR)\n      paint()\n      if (!a && !b) { raf = 0; return }\n      raf = requestAnimationFrame(frame)\n    }\n\n    const kick = () => {\n      if (raf || !onScreen || still) return\n      raf = requestAnimationFrame(frame)\n    }\n\n    const onMove = (e) => {\n      const b = root.getBoundingClientRect()\n      // Fractional track coordinates, so the maths is independent of pixel size.\n      tx = ((e.clientX - b.left) / Math.max(1, b.width)) * cw.length\n      ty = ((e.clientY - b.top) / Math.max(1, b.height)) * rw.length\n      kick()\n    }\n\n    const onLeave = () => {\n      tx = -1e5\n      ty = -1e5\n      kick()\n    }\n\n    const io = new IntersectionObserver(([entry]) => {\n      onScreen = entry.isIntersecting\n      if (onScreen) kick()\n      else if (raf) { cancelAnimationFrame(raf); raf = 0 }\n    })\n    io.observe(root)\n\n    paint()\n\n    if (hoverable && !still) {\n      root.addEventListener('pointermove', onMove, { passive: true })\n      root.addEventListener('pointerleave', onLeave, { passive: true })\n    }\n\n    return () => {\n      io.disconnect()\n      root.removeEventListener('pointermove', onMove)\n      root.removeEventListener('pointerleave', onLeave)\n      if (raf) cancelAnimationFrame(raf)\n    }\n  }, [columns, rows])\n\n  return (\n    <div\n      ref={rootRef}\n      className={`weight-grid ${className}`.trim()}\n      style={{ gap: `${gap}px`, ...style }}\n      {...rest}\n    >\n      {kids}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/weight-grid/WeightGrid.jsx"
    },
    {
      "path": "src/components/WeightGrid/WeightGrid.css",
      "content": "/* WeightGrid — JS writes only grid-template-rows/columns. The browser does the\n   layout, so cells keep real flow content and text never scales. */\n\n.weight-grid {\n  display: grid;\n  width: 100%;\n  height: 100%;\n}\n\n/* No transition on the template: the frame loop is already interpolating the\n   weights, and a CSS transition on top would be a second clock on one motion. */\n\n@media (prefers-reduced-motion: reduce) {\n  .weight-grid { /* the loop never starts; the grid stays even */ }\n}\n",
      "type": "registry:file",
      "target": "components/weight-grid/WeightGrid.css"
    },
    {
      "path": "src/components/WeightGrid/WeightGrid.d.ts",
      "content": "import type { ComponentPropsWithoutRef, ReactNode } from 'react'\n\nexport type Falloff = 'linear' | 'smooth' | 'sharp'\n\nexport interface WeightGridProps extends ComponentPropsWithoutRef<'div'> {\n  columns?: number\n  rows?: number\n  /** Extra weight the track nearest the cursor gains, before renormalising. */\n  gain?: number\n  /** Reach of the cursor's influence, in tracks. */\n  cursorRadius?: number\n  /** Per-frame approach rate. Lower is slower to settle. */\n  smoothing?: number\n  falloff?: Falloff\n  /** Grid gap, in px. */\n  gap?: number\n  children?: ReactNode\n}\n\ndeclare const WeightGrid: (props: WeightGridProps) => JSX.Element\nexport default WeightGrid\n",
      "type": "registry:file",
      "target": "components/weight-grid/WeightGrid.d.ts"
    }
  ],
  "type": "registry:component"
}