{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gravity-pile",
  "title": "Gravity Pile",
  "description": "Seeded circles, squares and triangles rain down, bounce off the floor and each other, and settle into a heap.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/gravity-pile/index.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  random,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type GravityPileColors = {\n  background: string;\n  shapes: string[];\n  accent: string;\n  floor: string;\n};\n\nexport type GravityPileProps = {\n  /** Number of falling bodies. */\n  count?: number;\n  seed?: number;\n  /** Gravity multiplier. */\n  gravity?: number;\n  /** Optional nodes dropped as round bodies, landing on top of the heap. */\n  items?: React.ReactNode[];\n  colors?: Partial<GravityPileColors>;\n  /** Color of the single accent body. Overrides `colors.accent`. */\n  accentColor?: string;\n  /** Background color. Overrides `colors.background`. */\n  backgroundColor?: string;\n  speed?: number;\n};\n\nexport const gravityPileDefaults = {\n  count: 30,\n  seed: 7,\n  gravity: 1,\n  colors: {\n    background: \"#111111\",\n    shapes: [\"#FFFFFF\", \"#C9C9C9\", \"#7A7A7A\"],\n    accent: \"#E5322D\",\n    floor: \"#FFFFFF\",\n  } satisfies GravityPileColors,\n  speed: 1,\n};\n\ntype Kind = \"circle\" | \"square\" | \"triangle\" | \"item\";\ntype Body = { r: number; kind: Kind; color: number; accent: boolean };\ntype Sim = { bodies: Body[]; frames: Float32Array[]; floor: number };\n\nconst FRAMES = 150;\nconst SUBSTEPS = 10;\nconst ITERATIONS = 4;\nconst BOUNCE = 0.42;\nconst FRICTION = 0.12;\nconst DAMPING = 0.999;\nconst GRIP = 0.45;\nconst SETTLE_FROM = 46;\nconst SETTLE_FRAMES = 18;\n\n// Verlet integration with position-based circle contacts. Runs once per\n// (size, count, seed, gravity), records every frame, and the component only\n// reads the table back.\nfunction simulate(\n  width: number,\n  height: number,\n  count: number,\n  seed: number,\n  gravity: number,\n  itemCount: number,\n): Sim {\n  const rnd = (key: string, i: number) =>\n    random(`gravity-pile-${seed}-${key}-${i}`);\n  const floor = height * 0.9;\n  const g = (height / 200) * gravity;\n  const dt = 1 / SUBSTEPS;\n  // Weaker gravity means a slower rain, so the heap settles later.\n  const settleFrom = SETTLE_FROM / Math.sqrt(Math.max(0.2, gravity));\n  const kinds: Kind[] = [\"circle\", \"square\", \"triangle\"];\n  const accentIndex = Math.max(0, count - itemCount - 3);\n  const bodies: Body[] = Array.from({ length: count }, (_, i) => ({\n    r: height * (0.036 + rnd(\"r\", i) * 0.032),\n    kind: i >= count - itemCount ? \"item\" : kinds[Math.floor(rnd(\"k\", i) * 3)],\n    color: Math.floor(rnd(\"c\", i) * 3),\n    accent: i === accentIndex,\n  }));\n  const n = bodies.length;\n  const x = new Float64Array(n);\n  const y = new Float64Array(n);\n  const px = new Float64Array(n);\n  const py = new Float64Array(n);\n  const angle = new Float64Array(n);\n  const spin = new Float64Array(n);\n  // Spawn in a tall column above the frame, rejecting overlaps so nothing\n  // explodes apart on the first step.\n  for (let i = 0; i < n; i++) {\n    const r = bodies[i].r;\n    const band = height * (0.1 + (i / Math.max(1, n)) * 2);\n    for (let attempt = 0; attempt < 40; attempt++) {\n      x[i] = width * (0.5 + (rnd(`x${attempt}`, i) - 0.5) * 0.26);\n      y[i] = -r - band - rnd(`y${attempt}`, i) * height * 0.25;\n      let clear = true;\n      for (let j = 0; j < i && clear; j++) {\n        const gap = bodies[i].r + bodies[j].r + 2;\n        clear = (x[i] - x[j]) ** 2 + (y[i] - y[j]) ** 2 > gap * gap;\n      }\n      if (clear) break;\n    }\n    px[i] = x[i] - (rnd(\"vx\", i) - 0.5) * 0.6 * dt;\n    py[i] = y[i] - height * 0.004 * dt;\n    angle[i] = rnd(\"a\", i) * Math.PI * 2;\n    spin[i] = (rnd(\"s\", i) - 0.5) * 0.02;\n  }\n\n  const frames: Float32Array[] = [];\n  const record = () => {\n    const out = new Float32Array(n * 3);\n    for (let i = 0; i < n; i++) {\n      out[i * 3] = x[i];\n      out[i * 3 + 1] = y[i];\n      out[i * 3 + 2] = angle[i];\n    }\n    frames.push(out);\n  };\n  record();\n\n  for (let f = 1; f <= FRAMES; f++) {\n    // Once damped out, the heap sleeps: no more solver creep.\n    if (f > settleFrom + SETTLE_FRAMES) {\n      frames.push(frames[frames.length - 1]);\n      continue;\n    }\n    for (let s = 0; s < SUBSTEPS; s++) {\n      // Late in the sim, bleed energy off so the heap comes fully to rest\n      // instead of creeping forever.\n      const settle = Math.min(1, Math.max(0, (f - settleFrom) / SETTLE_FRAMES));\n      const damping = DAMPING - settle * 0.12;\n      for (let i = 0; i < n; i++) {\n        const vx = (x[i] - px[i]) * damping;\n        const vy = (y[i] - py[i]) * damping;\n        px[i] = x[i];\n        py[i] = y[i];\n        x[i] += vx;\n        y[i] += vy + g * dt * dt;\n      }\n      for (let it = 0; it < ITERATIONS; it++) {\n        for (let i = 0; i < n; i++) {\n          for (let j = i + 1; j < n; j++) {\n            const dx = x[j] - x[i];\n            const dy = y[j] - y[i];\n            const min = bodies[i].r + bodies[j].r;\n            const d2 = dx * dx + dy * dy;\n            if (d2 >= min * min || d2 === 0) continue;\n            const d = Math.sqrt(d2);\n            const push = (min - d) / d;\n            const mi = bodies[i].r ** 2;\n            const mj = bodies[j].r ** 2;\n            const wi = mj / (mi + mj);\n            const wj = mi / (mi + mj);\n            x[i] -= dx * push * wi;\n            y[i] -= dy * push * wi;\n            x[j] += dx * push * wj;\n            y[j] += dy * push * wj;\n          }\n        }\n        for (let i = 0; i < n; i++) {\n          const r = bodies[i].r;\n          if (y[i] + r > floor) {\n            const vy = y[i] - py[i];\n            y[i] = floor - r;\n            // Reflect only real impacts so resting bodies do not buzz.\n            py[i] = vy > g * dt * 4 ? y[i] + vy * BOUNCE : y[i];\n            px[i] = x[i] - (x[i] - px[i]) * (1 - FRICTION);\n          }\n          if (x[i] - r < 0) {\n            x[i] = r;\n            px[i] = x[i] + (x[i] - px[i]) * BOUNCE;\n          }\n          if (x[i] + r > width) {\n            x[i] = width - r;\n            px[i] = x[i] + (x[i] - px[i]) * BOUNCE;\n          }\n        }\n      }\n      // Contact friction: damp relative sliding so bodies stack instead of\n      // flowing apart like marbles.\n      for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n          const dx = x[j] - x[i];\n          const dy = y[j] - y[i];\n          const min = bodies[i].r + bodies[j].r + 0.5;\n          const d2 = dx * dx + dy * dy;\n          if (d2 >= min * min || d2 === 0) continue;\n          const d = Math.sqrt(d2);\n          const tx = -dy / d;\n          const ty = dx / d;\n          const rel =\n            (x[j] - px[j] - (x[i] - px[i])) * tx +\n            (y[j] - py[j] - (y[i] - py[i])) * ty;\n          const mi = bodies[i].r ** 2;\n          const mj = bodies[j].r ** 2;\n          const k = rel * GRIP;\n          px[i] -= tx * k * (mj / (mi + mj));\n          py[i] -= ty * k * (mj / (mi + mj));\n          px[j] += tx * k * (mi / (mi + mj));\n          py[j] += ty * k * (mi / (mi + mj));\n        }\n      }\n      for (let i = 0; i < n; i++) {\n        const r = bodies[i].r;\n        const vx = x[i] - px[i];\n        // Bodies near the floor or the heap roll; free-falling ones keep spinning.\n        const grounded =\n          y[i] + r > floor - 1 || Math.abs(y[i] - py[i]) < g * dt;\n        if (grounded) spin[i] = Math.abs(vx) > 0.05 ? vx / r : spin[i] * 0.8;\n        angle[i] += spin[i];\n      }\n    }\n    record();\n  }\n  return { bodies, frames, floor };\n}\n\nfunction Shape({ body, fill }: { body: Body; fill: string }) {\n  const r = body.r;\n  if (body.kind === \"circle\") return <circle r={r} fill={fill} />;\n  if (body.kind === \"square\") {\n    const h = r * 0.84;\n    return (\n      <rect\n        x={-h}\n        y={-h}\n        width={h * 2}\n        height={h * 2}\n        rx={r * 0.12}\n        fill={fill}\n      />\n    );\n  }\n  const c = r * 1.12;\n  const pts = [0, 1, 2]\n    .map((k) => {\n      const a = -Math.PI / 2 + (k * Math.PI * 2) / 3;\n      return `${(Math.cos(a) * c).toFixed(2)},${(Math.sin(a) * c + r * 0.12).toFixed(2)}`;\n    })\n    .join(\" \");\n  return (\n    <polygon\n      points={pts}\n      fill={fill}\n      strokeLinejoin=\"round\"\n      stroke={fill}\n      strokeWidth={r * 0.14}\n    />\n  );\n}\n\nexport function GravityPile({\n  count = gravityPileDefaults.count,\n  seed = gravityPileDefaults.seed,\n  gravity = gravityPileDefaults.gravity,\n  items,\n  colors: colorOverrides,\n  accentColor,\n  backgroundColor,\n  speed = gravityPileDefaults.speed,\n}: GravityPileProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const baseColors = { ...gravityPileDefaults.colors, ...colorOverrides };\n  const colors = {\n    ...baseColors,\n    accent: accentColor ?? baseColors.accent,\n    background: backgroundColor ?? baseColors.background,\n  };\n  const itemCount = Math.min(\n    items?.length ?? 0,\n    Math.max(1, Math.round(count)),\n  );\n  const sim = useMemo(\n    () =>\n      simulate(\n        width,\n        height,\n        Math.max(1, Math.round(count)),\n        seed,\n        gravity,\n        itemCount,\n      ),\n    [width, height, count, seed, gravity, itemCount],\n  );\n\n  const t = Math.min(FRAMES, Math.max(0, (frame * speed * 30) / fps));\n  const f0 = Math.floor(t);\n  const f1 = Math.min(FRAMES, f0 + 1);\n  const mix = t - f0;\n  const a = sim.frames[f0];\n  const b = sim.frames[f1];\n  const n = sim.bodies.length;\n\n  return (\n    <AbsoluteFill\n      style={{ backgroundColor: colors.background, overflow: \"hidden\" }}\n    >\n      <svg\n        width={width}\n        height={height}\n        viewBox={`0 0 ${width} ${height}`}\n        style={{ position: \"absolute\", inset: 0 }}\n        aria-hidden\n      >\n        <line\n          x1={0}\n          x2={width}\n          y1={sim.floor + 1}\n          y2={sim.floor + 1}\n          stroke={colors.floor}\n          strokeOpacity={0.25}\n          strokeWidth={2}\n        />\n        {sim.bodies.map((body, i) => {\n          if (body.kind === \"item\") return null;\n          const x = a[i * 3] + (b[i * 3] - a[i * 3]) * mix;\n          const y = a[i * 3 + 1] + (b[i * 3 + 1] - a[i * 3 + 1]) * mix;\n          const rot = a[i * 3 + 2] + (b[i * 3 + 2] - a[i * 3 + 2]) * mix;\n          const fill = body.accent\n            ? colors.accent\n            : colors.shapes[body.color % colors.shapes.length];\n          return (\n            <g\n              key={i}\n              transform={`translate(${x.toFixed(2)} ${y.toFixed(2)}) rotate(${((rot * 180) / Math.PI).toFixed(2)})`}\n            >\n              <Shape body={body} fill={fill} />\n            </g>\n          );\n        })}\n      </svg>\n      {items?.slice(0, itemCount).map((item, j) => {\n        const i = n - itemCount + j;\n        const body = sim.bodies[i];\n        const x = a[i * 3] + (b[i * 3] - a[i * 3]) * mix;\n        const y = a[i * 3 + 1] + (b[i * 3 + 1] - a[i * 3 + 1]) * mix;\n        const rot = a[i * 3 + 2] + (b[i * 3 + 2] - a[i * 3 + 2]) * mix;\n        return (\n          <div\n            key={j}\n            style={{\n              position: \"absolute\",\n              left: x - body.r,\n              top: y - body.r,\n              width: body.r * 2,\n              height: body.r * 2,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              transform: `rotate(${rot}rad)`,\n            }}\n          >\n            {item}\n          </div>\n        );\n      })}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/gravity-pile.tsx"
    }
  ],
  "type": "registry:component"
}
