{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "life-grid",
  "title": "Life Grid",
  "description": "Conway's Game of Life plays out on a seeded grid, with newborn cells flashing in the accent.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/life-grid/index.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  random,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type LifeGridProps = {\n  /** Columns; rows follow the frame aspect ratio. */\n  cols?: number;\n  /** Share of cells alive in the seeded first generation, 0 to 1. */\n  density?: number;\n  /** Frames each generation is held. */\n  framesPerGeneration?: number;\n  /** Gap between tiles, as a fraction of the cell. */\n  gap?: number;\n  /** Draw the faint lattice of empty cells. */\n  showGrid?: boolean;\n  seed?: number;\n  speed?: number;\n  background?: string;\n  /** Living cells. */\n  color?: string;\n  /** Flash color for newborn cells. */\n  accent?: string;\n};\n\nexport const lifeGridDefaults: Required<LifeGridProps> = {\n  cols: 48,\n  density: 0.24,\n  framesPerGeneration: 5,\n  gap: 0.16,\n  showGrid: true,\n  seed: 7,\n  speed: 1,\n  background: \"#111111\",\n  color: \"#FFFFFF\",\n  accent: \"#E5322D\",\n};\n\nconst fmt = (n: number) => Math.round(n * 10) / 10;\nconst grow = Easing.out(Easing.back(2));\nconst shrink = Easing.in(Easing.cubic);\nconst settle = Easing.inOut(Easing.cubic);\n\n// Generations are a pure function of the seed and grid size; computed once and\n// shared across frames (and component instances) of a render.\nconst cache = new Map<string, Uint8Array[]>();\n\nfunction generations(\n  cols: number,\n  rows: number,\n  density: number,\n  seed: number,\n  count: number,\n) {\n  const key = `${cols}x${rows}|${density}|${seed}`;\n  const known = cache.get(key);\n  if (known && known.length >= count) return known;\n  const list: Uint8Array[] = known ?? [];\n  if (list.length === 0) {\n    const first = new Uint8Array(cols * rows);\n    for (let i = 0; i < first.length; i++) {\n      first[i] = random(`life-grid-${seed}-${i}`) < density ? 1 : 0;\n    }\n    list.push(first);\n  }\n  while (list.length < count) {\n    const prev = list[list.length - 1];\n    const next = new Uint8Array(cols * rows);\n    for (let y = 0; y < rows; y++) {\n      for (let x = 0; x < cols; x++) {\n        let n = 0;\n        for (let dy = -1; dy <= 1; dy++) {\n          for (let dx = -1; dx <= 1; dx++) {\n            if (dx === 0 && dy === 0) continue;\n            // Wrap around the edges (torus) so the board never runs dry at the border.\n            const xx = (x + dx + cols) % cols;\n            const yy = (y + dy + rows) % rows;\n            n += prev[yy * cols + xx];\n          }\n        }\n        const alive = prev[y * cols + x] === 1;\n        next[y * cols + x] = n === 3 || (alive && n === 2) ? 1 : 0;\n      }\n    }\n    list.push(next);\n  }\n  if (cache.size > 16) cache.clear();\n  cache.set(key, list);\n  return list;\n}\n\nexport function LifeGrid({\n  cols = lifeGridDefaults.cols,\n  density = lifeGridDefaults.density,\n  framesPerGeneration = lifeGridDefaults.framesPerGeneration,\n  gap = lifeGridDefaults.gap,\n  showGrid = lifeGridDefaults.showGrid,\n  seed = lifeGridDefaults.seed,\n  speed = lifeGridDefaults.speed,\n  background = lifeGridDefaults.background,\n  color = lifeGridDefaults.color,\n  accent = lifeGridDefaults.accent,\n}: LifeGridProps) {\n  const frame = useCurrentFrame();\n  const { width, height, durationInFrames } = useVideoConfig();\n  const nx = Math.max(4, Math.round(cols));\n  const ny = Math.max(4, Math.round((nx * height) / width));\n  const fpg = Math.max(1, framesPerGeneration);\n  const total = Math.ceil((durationInFrames * Math.max(0, speed)) / fpg) + 2;\n  const gens = useMemo(\n    () => generations(nx, ny, density, seed, total),\n    [nx, ny, density, seed, total],\n  );\n\n  const cell = Math.min(width / nx, height / ny);\n  const ox = (width - cell * nx) / 2;\n  const oy = (height - cell * ny) / 2;\n  const tile = cell * (1 - gap);\n\n  const time = Math.max(0, frame * speed);\n  const g = Math.min(gens.length - 1, Math.floor(time / fpg));\n  const local = Math.min(1, (time - g * fpg) / fpg);\n  const now = gens[g];\n  const before = g > 0 ? gens[g - 1] : now;\n\n  const square = (index: number, size: number) => {\n    const x = ox + (index % nx) * cell + (cell - size) / 2;\n    const y = oy + Math.floor(index / nx) * cell + (cell - size) / 2;\n    const s = fmt(size);\n    return `M ${fmt(x)} ${fmt(y)} h ${s} v ${s} h ${-s} Z `;\n  };\n\n  const bornSize = tile * (0.35 + 0.65 * grow(local));\n  const dyingSize = tile * (1 - shrink(Math.min(1, local * 1.6)));\n  let lattice = \"\";\n  let alive = \"\";\n  let fresh = \"\";\n  // Newborns pop in as accent tiles, then a white tile grows from their\n  // centre: a crisp flash with no pink cross-fade in between.\n  const coreSize =\n    tile * settle(Math.min(1, Math.max(0, (local - 0.35) / 0.65)));\n  for (let i = 0; i < now.length; i++) {\n    if (showGrid) lattice += square(i, tile);\n    if (now[i]) {\n      if (!before[i] && g > 0) {\n        fresh += square(i, bornSize);\n        if (coreSize > 0.5) alive += square(i, coreSize);\n      } else {\n        alive += square(i, tile);\n      }\n    } else if (before[i] && dyingSize > 0.5) {\n      alive += square(i, dyingSize);\n    }\n  }\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: background }}>\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        viewBox={`0 0 ${width} ${height}`}\n        role=\"img\"\n        aria-label=\"Game of Life grid\"\n        style={{ display: \"block\" }}\n      >\n        {showGrid ? <path d={lattice} fill={color} fillOpacity={0.05} /> : null}\n        <path d={fresh} fill={accent} />\n        <path d={alive} fill={color} />\n      </svg>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/life-grid.tsx"
    }
  ],
  "type": "registry:component"
}
