{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "galton-stack",
  "title": "Galton Stack",
  "description": "Dots fall through a Galton board into a bell curve, marked at 95%.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/galton-stack/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Geist\";\nimport { loadFont as loadMono } from \"@remotion/google-fonts/GeistMono\";\nimport { useId, useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  interpolateColors,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: sans } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: mono } = loadMono(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\n// gradients.sh \"limb\" palette: #e9ecff, #8a8bff, #4b2bd6, #160c3a.\nconst LIMB = {\n  dark: [\"#e9ecff\", \"#8a8bff\", \"#4b2bd6\"],\n  light: [\"#8a8bff\", \"#4b2bd6\", \"#2a1a7a\"],\n} as const;\n\nexport interface GaltonStackProps {\n  count?: number;\n  marker?: number;\n  label?: string;\n  markerLabel?: string;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst ROWS = 14;\nconst BINS = ROWS + 1;\nconst COLS = 4;\nconst SPAWN0 = 10;\nconst SPAWN_WIN = 50;\nconst DROP = 6;\nconst ROW_T = 1.3;\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    fg: \"#F3F2EF\",\n    muted: \"#8B8A90\",\n    axis: \"rgba(255,255,255,0.2)\",\n    peg: \"rgba(255,255,255,0.16)\",\n    marker: \"rgba(243,242,239,0.7)\",\n    dot: \"#D9D7D2\",\n    tail: \"#4A4950\",\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    axis: \"rgba(18,18,20,0.2)\",\n    peg: \"rgba(18,18,20,0.18)\",\n    marker: \"rgba(18,18,20,0.7)\",\n    dot: \"#3A3936\",\n    tail: \"#C4C2BC\",\n    grain: 0.045,\n  },\n} as const;\n\nfunction hash(a: number, b: number) {\n  let h = (Math.imul(a, 374761393) + Math.imul(b, 668265263)) | 0;\n  h = Math.imul(h ^ (h >>> 13), 1274126177);\n  h ^= h >>> 16;\n  return (h >>> 0) / 4294967296;\n}\n\nfunction choose(n: number, k: number) {\n  let r = 1;\n  for (let i = 1; i <= k; i++) r = (r * (n - k + i)) / i;\n  return r;\n}\n\ninterface Drop {\n  bin: number;\n  slot: number;\n  path: number[];\n  spawn: number;\n}\n\nfunction buildDrops(count: number): Drop[] {\n  const denom = 2 ** ROWS;\n  const target = Array.from({ length: BINS }, (_, b) =>\n    Math.round((count * choose(ROWS, b)) / denom),\n  );\n  const diff = count - target.reduce((s, x) => s + x, 0);\n  target[ROWS / 2] += diff;\n  const bins: number[] = [];\n  target.forEach((n, b) => {\n    for (let i = 0; i < n; i++) bins.push(b);\n  });\n  for (let i = bins.length - 1; i > 0; i--) {\n    const j = Math.floor(hash(i, 7) * (i + 1));\n    [bins[i], bins[j]] = [bins[j], bins[i]];\n  }\n  const filled = new Array(BINS).fill(0);\n  return bins.map((bin, i) => {\n    const moves = Array.from({ length: ROWS }, (_, r): number =>\n      r < bin ? 1 : 0,\n    );\n    for (let r = moves.length - 1; r > 0; r--) {\n      const j = Math.floor(hash(i * 31 + r, 13) * (r + 1));\n      [moves[r], moves[j]] = [moves[j], moves[r]];\n    }\n    const path = [0];\n    for (const m of moves) path.push(path[path.length - 1] + (m ? 0.5 : -0.5));\n    return {\n      bin,\n      slot: filled[bin]++,\n      path,\n      spawn: SPAWN0 + (i * SPAWN_WIN) / Math.max(1, bins.length - 1),\n    };\n  });\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `g${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, opacity }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={17}\n          stitchTiles=\"stitch\"\n        />\n        <feColorMatrix values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0.9 0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\nexport function GaltonStack({\n  count = 260,\n  marker = 0.733,\n  label = \"95% of renders under 2s\",\n  markerLabel = \"2s\",\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: GaltonStackProps) {\n  const t = useCurrentFrame() * speed;\n  const { width, height } = useVideoConfig();\n  const u = Math.min(width / 1280, height / 720);\n  const p = THEMES[theme];\n  const n = Math.max(20, Math.min(600, Math.round(count)));\n  const drops = useMemo(() => buildDrops(n), [n]);\n\n  const binP = 52 * u;\n  const d = 11 * u;\n  const r = 4.6 * u;\n  const pegR = 2.2 * u;\n  const cx = width / 2;\n  const row0 = height * 0.17;\n  const emitY = row0 - 34 * u;\n  const rowGap = 17 * u;\n  const baseY = height * 0.84;\n  const exitY = row0 + ROWS * rowGap;\n  const lift = pegR + r + 1 * u;\n  const binX = (b: number) => cx + (b - ROWS / 2) * binP;\n\n  const maxSlots = Math.max(\n    ...Array.from({ length: BINS }, (_, b) =>\n      drops.reduce((s, x) => s + (x.bin === b ? 1 : 0), 0),\n    ),\n  );\n  const stackTop = baseY - Math.ceil(maxSlots / COLS) * d;\n\n  const slotPos = (dr: Drop) => ({\n    x: binX(dr.bin) + ((dr.slot % COLS) - (COLS - 1) / 2) * d,\n    y: baseY - (Math.floor(dr.slot / COLS) + 0.5) * d - 1 * u,\n  });\n  const fallDur = (y: number) =>\n    5 + Math.sqrt(Math.max(0, y - exitY) / u) * 0.5;\n  const flight = (dr: Drop) => DROP + ROWS * ROW_T + fallDur(slotPos(dr).y);\n  const landEnd = Math.max(...drops.map((dr) => dr.spawn + flight(dr)));\n  const MARK = landEnd + 2;\n\n  const boundary = Math.max(1, Math.min(BINS - 1, Math.round(marker * BINS)));\n  const markerX = cx + (boundary - BINS / 2) * binP;\n\n  const m = /^(\\S*\\d\\S*)\\s+(.+)$/.exec(label.trim());\n  const hero = m ? m[1] : label;\n  const sub = m ? m[2] : \"\";\n  const heroSize = 132 * u;\n  const subSize = 24 * u;\n  const heroW = hero.length * 0.56 * heroSize;\n  const subW = sub.length * 0.5 * subSize;\n  const blockW = Math.max(heroW, subW);\n  const gapX = 22 * u;\n  const leftSide = markerX - gapX - blockW >= width * 0.08;\n  const labelBottom = stackTop - 40 * u;\n  const blockH = heroSize * 0.9 + 12 * u + subSize * 1.2;\n  const labelTop = labelBottom - blockH;\n  const bottomEdge = baseY + 14 * u + 20 * u;\n  const finalCenter = (labelTop + bottomEdge) / 2;\n  const shift = height / 2 - finalCenter;\n\n  const fieldIn = interpolate(t, [0, 14], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const fieldOut = interpolate(t, [MARK - 8, MARK + 8], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const field = fieldIn * (1 - fieldOut);\n  const cam = interpolate(t, [MARK - 6, MARK + 26], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const camScale = 1 + 0.05 * cam;\n  const lineIn = interpolate(t, [MARK + 2, MARK + 20], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const land = interpolate(t, [MARK + 14, MARK + 34], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const subIn = interpolate(t, [MARK + 22, MARK + 40], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const tickIn = interpolate(t, [MARK + 10, MARK + 24], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  const sw = Math.max(1, u);\n  const axisL = binX(0) - binP / 2;\n  const axisR = binX(ROWS) + binP / 2;\n  const axisIn = interpolate(t, [2, 22], [0, 1], { ...CLAMP, easing: EASE_IO });\n\n  const pegs: { x: number; y: number }[] = [];\n  for (let j = 0; j < ROWS; j++) {\n    for (let k = 0; k <= j; k++) {\n      pegs.push({ x: cx + (k - j / 2) * binP, y: row0 + j * rowGap });\n    }\n  }\n\n  const gid = `limb${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const stops = LIMB[theme];\n  // The gradient drifts a few percent sideways across the shot, never more.\n  const drift = interpolate(t, [0, 180], [-0.04, 0.04], CLAMP);\n  const heroFill = `linear-gradient(${172 + drift * 60}deg, ${stops[0]} 8%, ${stops[1]} 52%, ${stops[2]} 100%)`;\n\n  const dotsSvg = drops.map((dr, i) => {\n    const lt = t - dr.spawn;\n    if (lt < 0) return null;\n    const end = slotPos(dr);\n    let x = cx;\n    let y = emitY;\n    let landed = false;\n    if (lt < DROP) {\n      const k = (lt / DROP) ** 2;\n      y = emitY + (row0 - lift - emitY) * k;\n    } else if (lt < DROP + ROWS * ROW_T) {\n      const s = (lt - DROP) / ROW_T;\n      const j = Math.min(ROWS - 1, Math.floor(s));\n      const tau = s - j;\n      const ex = tau * tau * (3 - 2 * tau);\n      x = cx + (dr.path[j] + (dr.path[j + 1] - dr.path[j]) * ex) * binP;\n      y =\n        row0 -\n        lift +\n        (j + tau) * rowGap -\n        Math.sin(Math.PI * tau) * 3.5 * u * (1 - j / ROWS);\n    } else {\n      const fd = fallDur(end.y);\n      const k = Math.min(1, (lt - DROP - ROWS * ROW_T) / fd);\n      const sx = cx + dr.path[ROWS] * binP;\n      const sy = row0 - lift + ROWS * rowGap;\n      x = sx + (end.x - sx) * Math.min(1, k * 1.6);\n      y = sy + (end.y - sy) * k * k;\n      landed = k >= 1;\n    }\n    const a = interpolate(\n      t,\n      [MARK + 6 + dr.bin * 1.8, MARK + 11 + dr.bin * 1.8],\n      [0, 1],\n      { ...CLAMP, easing: EASE_IO },\n    );\n    const hot = landed && dr.bin < boundary;\n    const fill =\n      landed && !hot ? interpolateColors(a, [0, 1], [p.dot, p.tail]) : p.dot;\n    return (\n      <g key={`d${i}`} opacity={Math.min(1, lt / 2)}>\n        <circle cx={x} cy={y} r={r} fill={fill} />\n        {hot && a > 0 ? (\n          <circle cx={x} cy={y} r={r} fill={`url(#${gid})`} opacity={a} />\n        ) : null}\n      </g>\n    );\n  });\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          transform: `translateY(${shift * cam}px) scale(${camScale})`,\n          transformOrigin: `50% ${finalCenter}px`,\n        }}\n      >\n        <svg\n          width={width}\n          height={height}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          <defs>\n            <linearGradient\n              id={gid}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={cx + drift * 0.25 * width}\n              y1={stackTop - 6 * u}\n              x2={cx - drift * 0.25 * width}\n              y2={baseY}\n            >\n              <stop offset=\"0\" stopColor={stops[0]} />\n              <stop offset=\"0.45\" stopColor={stops[1]} />\n              <stop offset=\"1\" stopColor={stops[2]} />\n            </linearGradient>\n          </defs>\n          <g opacity={field}>\n            <circle\n              cx={cx}\n              cy={emitY}\n              r={7 * u}\n              fill=\"none\"\n              stroke={accent}\n              strokeWidth={sw * 1.2}\n            />\n            {pegs.map((pg) => (\n              <circle\n                key={`p${pg.x.toFixed(1)}-${pg.y.toFixed(1)}`}\n                cx={pg.x}\n                cy={pg.y}\n                r={pegR}\n                fill={p.peg}\n              />\n            ))}\n          </g>\n          <line\n            x1={cx - ((axisR - axisL) / 2) * axisIn}\n            x2={cx + ((axisR - axisL) / 2) * axisIn}\n            y1={baseY + sw / 2}\n            y2={baseY + sw / 2}\n            stroke={p.axis}\n            strokeWidth={sw}\n          />\n          {dotsSvg}\n          {lineIn > 0 ? (\n            <line\n              x1={markerX}\n              x2={markerX}\n              y1={baseY + 8 * u}\n              y2={baseY + 8 * u - (baseY + 8 * u - labelTop) * lineIn}\n              stroke={p.marker}\n              strokeWidth={sw * 1.5}\n            />\n          ) : null}\n        </svg>\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: markerX,\n            top: baseY + 14 * u,\n            transform: `translate(-50%, ${(1 - tickIn) * 6 * u}px)`,\n            fontFamily: mono,\n            fontWeight: 500,\n            fontSize: 17 * u,\n            letterSpacing: \"-0.01em\",\n            color: p.fg,\n            fontVariantNumeric: \"tabular-nums\",\n            whiteSpace: \"nowrap\",\n            opacity: tickIn,\n          }}\n        >\n          {markerLabel}\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            top: labelTop,\n            ...(leftSide\n              ? { right: width - markerX + gapX, alignItems: \"flex-end\" }\n              : { left: markerX + gapX, alignItems: \"flex-start\" }),\n            display: \"flex\",\n            flexDirection: \"column\",\n            gap: 12 * u,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          <div\n            style={{\n              fontFamily: sans,\n              fontWeight: 400,\n              fontSize: heroSize,\n              lineHeight: 0.9,\n              letterSpacing: \"-0.05em\",\n              paddingRight: \"0.04em\",\n              color: \"transparent\",\n              backgroundImage: heroFill,\n              WebkitBackgroundClip: \"text\",\n              backgroundClip: \"text\",\n              fontVariantNumeric: \"tabular-nums\",\n              opacity: land,\n              filter: land < 0.99 ? `blur(${(1 - land) * 8 * u}px)` : undefined,\n              transform: `translateY(${(1 - land) * 18 * u}px)`,\n            }}\n          >\n            {hero}\n          </div>\n          {sub ? (\n            <div\n              style={{\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 10 * u,\n                fontFamily: sans,\n                fontWeight: 400,\n                fontSize: subSize,\n                lineHeight: 1.2,\n                letterSpacing: \"-0.015em\",\n                color: p.muted,\n                opacity: subIn,\n                transform: `translateY(${(1 - subIn) * 8 * u}px)`,\n              }}\n            >\n              <span\n                style={{\n                  width: 10 * u,\n                  height: 10 * u,\n                  borderRadius: \"50%\",\n                  background: `linear-gradient(160deg, ${stops[0]}, ${stops[2]})`,\n                }}\n              />\n              {sub}\n            </div>\n          ) : null}\n        </div>\n      </AbsoluteFill>\n\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 85% 85% at 50% 50%, transparent 64%, ${p.bg} 100%)`,\n          opacity: 0.5,\n        }}\n      />\n      <Grain opacity={p.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/galton-stack.tsx"
    }
  ],
  "type": "registry:component"
}
