{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "count-grid",
  "title": "Count Grid",
  "description": "Number count-up grid: cards multiply and a counter jumps, the it-scales stat shot. A grid of cards rushes in under a horizontal smear, settles on five and a label, then the camera pulls back a fraction while every empty cell around them fills in from the middle out and the number jumps. The \"and it scales\" shot, without a cut — one card is deliberately late to land, which is what makes the entry read as five separate things arriving rather than one picture being pushed in.",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "https://snapcn.dev/r/snap-cn-ui.json"
  ],
  "files": [
    {
      "path": "registry/snap-cn/count-grid/index.tsx",
      "content": "// Count Grid · snapcn — https://snapcn.dev/docs/scenes/count-grid\n\"use client\";\n\nimport { loadFont as loadSans } from \"@remotion/google-fonts/Inter\";\nimport {\n  AbsoluteFill,\n  getRemotionEnvironment,\n  Img,\n  interpolate,\n  staticFile,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport {\n  Item,\n  mixOklch,\n  type SnapCnTheme,\n  useSnapCnTheme,\n} from \"@/lib/snap-cn-ui\";\n\n/**\n * A grid of cards that rushes in, settles on a small count, then fills the\n * frame while the number jumps.\n *\n * Five cards and a label; the camera pulls back a fraction, every empty cell\n * around them fills in from the middle out, and the label goes from five to\n * five hundred. It is the \"and it scales\" shot — one thing, then all of them,\n * without a cut.\n *\n * Beat by beat, measured off the reference at 30fps:\n *\n * | beat | frames | what moves |\n * | --- | --- | --- |\n * | rush in | 0 – 7 | cell pitch 184 → 138, hard horizontal blur |\n * | settle | 7 – 19 | 138 → 121, blur gone by 12 |\n * | label | 11 – 18 | the whole line fades up at once |\n * | drift | 19 – 36 | 121 → 118, still moving, never held |\n * | fill | 37 – 46 | 118 → 96.5, every cell populates, 5 → 500 |\n *\n * The pull-back is a *scale*, not a camera z-move: the two centre columns hold\n * their midpoint at x 358.5 for all 47 frames while the pitch between them\n * walks 138.5 → 96.5, and the cards keep a constant fraction of the cell. And\n * the settle never actually settles — the pitch is still shedding a fifth of a\n * pixel per frame at frame 37, which is what stops the middle of the shot\n * looking like a freeze-frame.\n */\n\nconst { fontFamily: SANS } = loadSans();\n\n/* ─────────────────────────────────────────────────────────────────────────\n   Geometry, in the reference's own 712×398 pixels\n   ───────────────────────────────────────────────────────────────────────── */\n\nexport const REF_W = 712;\nexport const REF_H = 398;\n\n/**\n * What the pull-back scales about.\n *\n * Horizontally it is the midpoint the two centre columns hold all clip: 358.75.\n * Vertically it is *not* the middle row — the centre row's own middle sits at\n * 202.5 and moves to 203.5 as the grid shrinks, which puts the fixed point 5px\n * lower, at 207.6. Scaling about the row instead leaves the whole grid a\n * pixel high through the entire fill.\n */\nexport const ORIGIN = { x: 358.75, y: 207.6 };\n\n/**\n * One cell at scale 1, and how much of it the card takes.\n *\n * The *cell* is measured — 120.5 × 147, the pitch the whole pull-back is fitted\n * to. The card inside it is a design choice and it is deliberately not the\n * reference's: measured, that card is 90.5 × 117 with a 30px gutter and a 6px\n * radius, which reads as a scatter of small rounded stickers. This one fills\n * 95% of its cell with a 3px corner, so the grid reads as a wall of frames.\n */\nexport const CELL = { w: 120.5, h: 147 };\nexport const CARD = { w: 114.5, h: 139.5, radius: 3 };\n\n/**\n * How much closer the grid sits than the reference's camera.\n *\n * One, now. It was 1.75 for exactly as long as it took to find out that the\n * cards were being *drawn* at reference-pixel size inside cells laid out at\n * render-pixel size — 0.42 of their own cell rather than 0.95 — and that no\n * amount of zoom fixes a missing unit conversion, it only moves the error into\n * the cell spacing as well. Left in place because it is the honest knob if the\n * grid should ever sit closer than the reference's, and because a `1` with this\n * note attached is cheaper than someone rediscovering the bug.\n */\nexport const GRID_ZOOM = 1;\n\n/** A column *boundary* sits on the centre; a row *centre* does. */\nexport const COL_EDGE = 359;\nexport const ROW_MID = 202.5;\n\n/**\n * The five that are there before the grid fills.\n *\n * `[col, row]`, with column 0 immediately right of the centre boundary and row\n * 0 the middle row. Four make a square around the label and the fifth hangs\n * below on the right — an even 2×2 reads as a swatch, and the odd one out is\n * what makes it read as a handful of people.\n */\nexport const SEED: readonly (readonly [number, number])[] = [\n  [-1, -1],\n  [0, -1],\n  [-1, 0],\n  [0, 0],\n  [0, 1],\n];\n\n/** The label, at scale 1. */\nexport const LABEL = {\n  cy: ROW_MID,\n  size: 30,\n  padX: 15,\n  height: 41,\n  radius: 9,\n};\n\n/* ─────────────────────────────────────────────────────────────────────────\n   Curves\n   ───────────────────────────────────────────────────────────────────────── */\n\n/** When the grid stops being five cards and becomes all of them. */\nexport const FILL_AT = 37;\n\n/**\n * The cell pitch, in reference px — the one number the whole scene hangs on.\n *\n * Tracked as the distance between the two centre columns: 138.5 at frame 7,\n * 128 at 11, 121.5 at 19, 120 at 26, 118 from 32 to 36, then 113, 104, 99.5,\n * 96.5 across the fill. Before frame 7 the horizontal blur is wider than a card\n * and nothing can be measured, so that stretch is the same exponential run\n * backwards, which puts frame 0 at 174 — a card and a half outside the frame.\n *\n * Knots, not that exponential. Two of them fitted the settle and the fill to\n * within a pixel and a half each, which is a pixel and a half of grid, and they\n * met 3.7px apart at the switch going the *wrong way* — the pitch grew by 0.17\n * between 36 and 37, so the grid jolted outward on the exact frame it was\n * supposed to start collapsing. Every knot below is a measured frame and the\n * table is strictly decreasing, so it cannot do that.\n *\n * Every frame number here is zero-based, and every one of them was read off a\n * file called `f0NN.png` that is one-based: `f038.png` is frame 37. Taking the\n * filenames at face value put the whole clip a frame late and cost the switch\n * frame alone 17 mean.\n */\nexport function pitch(t: number, fps = 30): number {\n  return interpolate(\n    t * fps,\n    [\n      0, 7, 9, 11, 13, 15, 17, 19, 22, 26, 29, 31, 36, 37, 38, 39, 40, 41, 42,\n      43, 45, 46,\n    ],\n    [\n      174, 138.5, 132, 128, 125, 124, 122.5, 121.5, 120, 119.6, 119, 118.5, 118,\n      117.9, 113, 106, 104, 102, 99.5, 97.5, 96.6, 96.5,\n    ],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n  );\n}\n\n/** The pitch as a multiple of the cell, which is what everything scales by. */\nexport function gridScale(t: number, fps = 30): number {\n  return pitch(t, fps) / CELL.w;\n}\n\n/**\n * The horizontal blur, in reference px.\n *\n * Measured as the frame's own horizontal gradient, which is what a smear\n * actually costs: 0.28 at frame 3, 1.90 at 4, 2.75 at 5, and then flat for ten\n * frames while it creeps to 3.18. So the blur is spent inside five frames and\n * the long ease that follows it is sharp.\n *\n * The first cut derived this from the pull-back's own speed, on the theory that\n * a blur and its motion should not be able to disagree. They can: the motion is\n * still shedding two pixels of pitch a frame at frame 8 and the reference is\n * already sharp there, so the derivative kept a smear on screen for seven\n * frames the reference does not have, and cost those frames 3 mean each.\n */\nexport function entryBlur(t: number, fps = 30): number {\n  return interpolate(t * fps, [0, 2, 3, 4, 5], [22, 14, 6, 2, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n}\n\n/**\n * The one card that arrives late, in reference px of extra smear.\n *\n * The scene's own blur is spent by frame 5, but the card behind the label is\n * still a horizontal streak at 7 and only lands around 9 — every other card is\n * sharp and at scale by 5. It is the detail that makes the entry read as five\n * separate things arriving rather than one picture being pushed in, and it is\n * the only per-card timing in the piece.\n */\nexport const LATE_CARD: readonly [number, number] = [-1, 0];\n\nexport function lateBlur(t: number, fps = 30): number {\n  return interpolate(t * fps, [3, 5, 7, 9], [26, 17, 7, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n}\n\n/** The label fading up, all of it at once. */\nexport function labelIn(t: number, fps = 30): number {\n  return interpolate(t * fps, [11, 13, 15, 17, 18], [0, 0.22, 0.55, 0.9, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n}\n\n/**\n * How present a cell is.\n *\n * `ring` is the Chebyshev distance to the nearest seed cell, so the ring\n * touching the five arrives first and each one after it a frame later. That\n * ordering is measured: at frame 37 exactly two new columns exist, either side\n * of the original two, and by 38 there are eight.\n */\nexport function cellIn(ring: number, t: number, fps = 30): number {\n  if (ring === 0) return 1;\n  // Half a frame per ring and two frames each: the reference is 90% done in\n  // three frames — 20.3, 19.0, 19.8 mean change and then a decay — and a\n  // ring-per-frame stagger over three turned that into a five-frame ramp that\n  // peaked two frames after the reference had finished.\n  // The ramp *straddles* the switch frame rather than starting on it: at frame\n  // 37 the reference's first new columns are already half there — 80px wide\n  // against a settled 90 — so a ring that is still zero on 37 arrives a whole\n  // frame late and the change profile peaks two frames after the reference's.\n  //\n  // A whole frame of stagger, not half. On frame 37 the reference has ring one\n  // and nothing else; by 38 it has rings one and two, the second of them\n  // narrower than the first. Half a frame put ring two on screen a frame early\n  // and drew cards where the reference still had page.\n  const at = FILL_AT - 1 + (ring - 1);\n  return interpolate(t * fps, [at, at + 1.5], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n}\n\n/** Chebyshev distance from a cell to the nearest of the five. */\nexport function ringOf(col: number, row: number): number {\n  let best = Number.POSITIVE_INFINITY;\n  for (const [c, r] of SEED) {\n    best = Math.min(best, Math.max(Math.abs(col - c), Math.abs(row - r)));\n  }\n  return best;\n}\n\n/* ─────────────────────────────────────────────────────────────────────────\n   Component\n   ───────────────────────────────────────────────────────────────────────── */\n\nexport interface CountGridProps {\n  /** What the label counts, before the grid fills and after. */\n  from?: string;\n  to?: string;\n  /** The noun after the number. */\n  noun?: string;\n  /** Card images, cycled across the grid. Anything `<Img>` can load. */\n  cards?: string[];\n  /** Defaults to the theme's `background`. */\n  background?: string;\n  /**\n   * The label's ink, left end and right end of its gradient. Default to\n   * `primary` walked halfway toward `foreground`, then `primary` — the navy →\n   * blue the reference had, but one that turns pale in dark mode instead of\n   * vanishing into it.\n   */\n  inkFrom?: string;\n  inkTo?: string;\n  theme?: Partial<SnapCnTheme>;\n  mode?: \"light\" | \"dark\";\n  /** 1 is the measured speed. */\n  speed?: number;\n}\n\n/**\n * The card faces: frames out of snapcn's own showcase clips, and one still per\n * component in the registry — which is what the label is counting.\n *\n * Through `staticFile`, not as a bare path. A Remotion bundle copies the public\n * directory to `<bundle>/public/` and serves it from there, so a hard-coded\n * `/demos/posters/x.webp` is a 404 in every render while looking perfect in the\n * app — `staticFile` is what knows the difference. None of these live in\n * `public/` any more: a fresh install has no snapcn `public/`, so a relative\n * path there failed every render outside this repo. All of them are full URLs\n * on snapcn's media host; `staticFile` is only for a user's own relative paths.\n *\n * Both kinds on purpose. Eight frames alone tile visibly across forty cells;\n * the registry's stills alone are 16:9 scenes of dark text on white, and a 3:4\n * crop of one is a blank card. Interleaved they read the way a real library of\n * clips does — some motion, some interface — and land between the two on\n * weight, which is also where the reference's grid of portraits sits.\n */\nconst ART = [\n  \"https://media.snapcn.dev/demos/showcase/438b9e6b50654a44d404fbf358c26e9f.webp\",\n  \"https://media.snapcn.dev/demos/showcase/5e5305b05bd405a0d89570725434099e.webp\",\n  \"https://media.snapcn.dev/demos/showcase/767d99bb371a54d0d36751e8cecae43c.jpg\",\n  \"https://media.snapcn.dev/demos/showcase/821d815affa6496c39cbdeeec7a84603.jpg\",\n  \"https://media.snapcn.dev/demos/showcase/937438c560ada1c83317f2c11b3454b0.jpg\",\n  \"https://media.snapcn.dev/demos/showcase/98f89cb9994f5c382ab964062c4039db.jpg\",\n  \"https://media.snapcn.dev/demos/showcase/b25b82db2892efff9be3204e860d30ee.jpg\",\n  \"https://media.snapcn.dev/demos/showcase/c9ebc6337aa2268ac4b357f9cb1ac547.jpg\",\n];\nconst STILLS = [\n  \"https://media.snapcn.dev/demos/posters/agent-steps.webp\",\n  \"https://media.snapcn.dev/demos/posters/announce-title.webp\",\n  \"https://media.snapcn.dev/demos/posters/answer-highlight.webp\",\n  \"https://media.snapcn.dev/demos/posters/answer-stream.webp\",\n  \"https://media.snapcn.dev/demos/posters/block-wordmark.webp\",\n  \"https://media.snapcn.dev/demos/posters/cursor-track.webp\",\n  \"https://media.snapcn.dev/demos/posters/follower-rush.webp\",\n  \"https://media.snapcn.dev/demos/posters/hero-launch.webp\",\n  \"https://media.snapcn.dev/demos/posters/karaoke-captions.webp\",\n  \"https://media.snapcn.dev/demos/posters/laptop-frame.webp\",\n  \"https://media.snapcn.dev/demos/posters/logo-assemble.webp\",\n  \"https://media.snapcn.dev/demos/posters/logo-drift.webp\",\n  \"https://media.snapcn.dev/demos/posters/logo-flicker.webp\",\n  \"https://media.snapcn.dev/demos/posters/moodboard-reveal.webp\",\n  \"https://media.snapcn.dev/demos/posters/orbit-gallery.webp\",\n  \"https://media.snapcn.dev/demos/posters/phone-frame.webp\",\n  \"https://media.snapcn.dev/demos/posters/prompt-send.webp\",\n  \"https://media.snapcn.dev/demos/posters/prompt-zoom.webp\",\n  \"https://media.snapcn.dev/demos/posters/punch-lines.webp\",\n  \"https://media.snapcn.dev/demos/posters/screen-recording.webp\",\n  \"https://media.snapcn.dev/demos/posters/search-typing.webp\",\n  \"https://media.snapcn.dev/demos/posters/status-cycle.webp\",\n  \"https://media.snapcn.dev/demos/posters/terminal-simulator.webp\",\n  \"https://media.snapcn.dev/demos/posters/text-build.webp\",\n  \"https://media.snapcn.dev/demos/posters/text-highlight.webp\",\n  \"https://media.snapcn.dev/demos/posters/text-reveal.webp\",\n  \"https://media.snapcn.dev/demos/posters/text-rewrite.webp\",\n  \"https://media.snapcn.dev/demos/posters/text-select.webp\",\n  \"https://media.snapcn.dev/demos/posters/text-swap.webp\",\n  \"https://media.snapcn.dev/demos/posters/text-swell.webp\",\n  \"https://media.snapcn.dev/demos/posters/type-morph.webp\",\n  \"https://media.snapcn.dev/demos/posters/word-captions.webp\",\n  \"https://media.snapcn.dev/demos/posters/word-flip.webp\",\n];\n// `concat(...map)`, not `flatMap`: a stock Remotion project types against\n// ES2015, where `flatMap` does not exist and the install shows red.\nconst OWN_CARDS = ([] as string[])\n  .concat(...STILLS.map((s, i) => [ART[i % ART.length] as string, s]))\n  .map((f) => (f.startsWith(\"https://\") ? f : staticFile(f)));\n// index 0, 2, 4 … are ART by construction, and the five seed cells only ever\n// reach the first `SEED.length` of them.\n\n/**\n * A root-relative asset is a `public/` file. Only the site's own Player serves\n * it at that path; Remotion Studio and a render know its URL through\n * `staticFile()` alone — so every environment but the Player rewrites it.\n */\nfunction resolveSrc(src: string): string {\n  const isLocal = src.startsWith(\"/\") && !src.startsWith(\"//\");\n  if (!isLocal || getRemotionEnvironment().isPlayer) return src;\n  // A value that already came out of staticFile() carries the static base\n  // (`/static-<hash>/…`); running it through again would prefix it twice.\n  const base = staticFile(\"_\").slice(0, -2);\n  if (base && src.startsWith(`${base}/`)) return src;\n  try {\n    return staticFile(src.replace(/^\\/+/, \"\"));\n  } catch {\n    return src;\n  }\n}\n\nexport function CountGrid({\n  from = \"5\",\n  to = \"500\",\n  noun = \"clips\",\n  cards = OWN_CARDS,\n  background,\n  inkFrom,\n  inkTo,\n  speed = 1,\n  theme,\n  mode,\n}: CountGridProps) {\n  const t = useSnapCnTheme(theme, mode);\n  const page = background ?? t.background;\n  const ink0 = inkFrom ?? mixOklch(t.primary, t.foreground, 0.5);\n  const ink1 = inkTo ?? t.primary;\n  // The card edge is half a hairline: forty full-strength borders read as a\n  // grid of boxes, not a wall of frames.\n  const cardEdge = mixOklch(t.card, t.border, 0.5);\n  const frame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n  const now = frame / fps / (speed || 1);\n\n  const k = Math.min(width / REF_W, height / REF_H);\n  const ox = (width - REF_W * k) / 2;\n  const oy = (height - REF_H * k) / 2;\n\n  const s = gridScale(now, fps);\n  // The cards live one zoom step closer than everything else — see GRID_ZOOM.\n  const g = s * GRID_ZOOM;\n  const blur = entryBlur(now, fps) * k;\n  const late = lateBlur(now, fps) * k;\n  const label = labelIn(now, fps);\n\n  // Everything is laid out at scale 1 and then scaled about ORIGIN, so a cell's\n  // place in the grid never depends on the zoom — only its size does.\n  const at = (v: number, origin: number, scale = s) =>\n    (v - origin) * scale + origin;\n  // `cellW`/`cellH` stay in reference px — they are only ever compared against\n  // REF_W/REF_H to count how many cells reach the frame.\n  const cellW = CELL.w * g;\n  const cellH = CELL.h * g;\n  // The card's size is drawn, so it needs `k` — the same reference→render scale\n  // its own position already goes through. Without it the card was laid out in\n  // reference px inside a cell laid out in render px: 0.42 of its cell on a\n  // 1280-wide render instead of 0.95, which is every \"the images are too small\n  // and too far apart\" note in one bug.\n  const cardW = CARD.w * g * k;\n  const cardH = CARD.h * g * k;\n\n  // How many cells reach the frame at this scale, plus one so nothing pops in\n  // at the edge.\n  const cols = Math.ceil(REF_W / cellW / 2) + 2;\n  const rows = Math.ceil(REF_H / cellH / 2) + 2;\n\n  const cells: {\n    col: number;\n    row: number;\n    x: number;\n    y: number;\n    o: number;\n    i: number;\n    src: string;\n  }[] = [];\n  for (let row = -rows; row <= rows; row++) {\n    for (let col = -cols; col <= cols; col++) {\n      const o = cellIn(ringOf(col, row), now, fps);\n      if (o <= 0) continue;\n      const cx = at(COL_EDGE + (col + 0.5) * CELL.w, ORIGIN.x, g) * k + ox;\n      const cy = at(ROW_MID + row * CELL.h, ORIGIN.y, g) * k + oy;\n      if (cx < -cardW || cx > width + cardW) continue;\n      if (cy < -cardH || cy > height + cardH) continue;\n      // A stable, well-mixed index: neighbours must not share a card or the\n      // grid reads as wallpaper.\n      //\n      // `cards` alternates the two kinds, so its even slots are the ones with\n      // pictures on them. The five that carry the whole first half of the shot\n      // take only those; everything else takes the mix. Sharing one index put\n      // three of the five on registry stills — 16:9 scenes of dark text on\n      // white, cropped to 3:4 — and the hero moment was two blank cards and a\n      // label.\n      const mix = col * 7 + row * 13;\n      // `col * 2 + row` is the one small combination that is injective over the\n      // five seed cells modulo five — 7/13 sends (-1,-1) and (0,0) to the same\n      // slot, and the two cards diagonally across the label came out identical.\n      const seed = col * 2 + row;\n      const i =\n        ringOf(col, row) === 0\n          ? (2 * (((seed % SEED.length) + SEED.length) % SEED.length)) %\n            cards.length\n          : ((mix % cards.length) + cards.length) % cards.length;\n      cells.push({ col, row, x: cx, y: cy, o, i, src: cards[i] ?? \"\" });\n    }\n  }\n\n  // The grid cycles `cards`, so one card is drawn many times. Studio outlines\n  // the copy nearest the middle of the frame; the others are only pictures of\n  // it.\n  const nearest = new Map<number, (typeof cells)[number]>();\n  const off = (c: (typeof cells)[number]) =>\n    Math.hypot(c.x - width / 2, c.y - height / 2);\n  for (const c of cells) {\n    const held = nearest.get(c.i);\n    if (!held || off(c) < off(held)) nearest.set(c.i, c);\n  }\n\n  const filled = now * fps >= FILL_AT;\n  const pillH = LABEL.height * s * k;\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: page,\n        fontFamily: SANS,\n        overflow: \"hidden\",\n      }}\n    >\n      {/* A directional blur: the motion is horizontal, and CSS `blur()` is\n          round, so it softens the card's top and bottom edges — which are not\n          moving — as much as its sides. */}\n      <svg width={0} height={0} style={{ position: \"absolute\" }} aria-hidden>\n        <title>Motion blur</title>\n        <filter\n          id=\"count-grid-smear\"\n          x=\"-40%\"\n          y=\"-10%\"\n          width=\"180%\"\n          height=\"120%\"\n        >\n          <feGaussianBlur stdDeviation={`${blur} 0`} />\n        </filter>\n        <filter\n          id=\"count-grid-late\"\n          x=\"-90%\"\n          y=\"-10%\"\n          width=\"280%\"\n          height=\"120%\"\n        >\n          <feGaussianBlur stdDeviation={`${late} 0`} />\n        </filter>\n      </svg>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          filter: blur > 0.35 ? \"url(#count-grid-smear)\" : undefined,\n          ...(getRemotionEnvironment().isRendering\n            ? null\n            : { willChange: \"filter\" as const }),\n        }}\n      >\n        {cells.map((c) => (\n          <Item\n            key={`${c.col},${c.row}`}\n            index={c.i}\n            primary={nearest.get(c.i) === c}\n          >\n            <div\n              style={{\n                position: \"absolute\",\n                left: c.x - cardW / 2,\n                top: c.y - cardH / 2,\n                width: cardW,\n                height: cardH,\n                borderRadius: CARD.radius * g * k,\n                overflow: \"hidden\",\n                background: t.card,\n                // A hairline, not a shadow: a drop shadow under forty white\n                // tiles on a white page is forty grey smears.\n                boxShadow: `inset 0 0 0 ${Math.max(0.5, 0.9 * g * k)}px ${cardEdge}`,\n                display: \"flex\",\n                alignItems: \"center\",\n                opacity: c.o,\n                filter:\n                  late > 0.35 &&\n                  c.col === LATE_CARD[0] &&\n                  c.row === LATE_CARD[1]\n                    ? \"url(#count-grid-late)\"\n                    : undefined,\n              }}\n            >\n              {c.src ? (\n                <Img\n                  src={resolveSrc(c.src)}\n                  style={{\n                    width: cardW,\n                    height: cardH,\n                    // Preflight's `img { max-width: 100% }` against a box that\n                    // sizes from its content is 100% of nothing — the card\n                    // renders zero px wide on the site and perfectly in the mp4.\n                    maxWidth: \"none\",\n                    objectFit: \"cover\",\n                  }}\n                  alt=\"\"\n                />\n              ) : null}\n            </div>\n          </Item>\n        ))}\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          right: 0,\n          top: at(LABEL.cy, ORIGIN.y) * k + oy - pillH / 2,\n          display: \"flex\",\n          justifyContent: \"center\",\n          opacity: label,\n        }}\n      >\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            height: pillH,\n            padding: `0 ${LABEL.padX * s * k}px`,\n            borderRadius: LABEL.radius * s * k,\n            background: t.card,\n            border: `1px solid ${t.border}`,\n            // Not a drop shadow: on a white page under a white pill it reads as\n            // a grey smear, and the reference has a hairline and nothing else.\n            boxShadow: \"none\",\n            fontSize: LABEL.size * s * k,\n            fontWeight: 400,\n            lineHeight: 1,\n            whiteSpace: \"nowrap\",\n            color: ink0,\n            textRendering: \"geometricPrecision\",\n          }}\n        >\n          <span\n            style={{\n              backgroundImage: `linear-gradient(90deg, ${ink0} 0%, ${ink1} 100%)`,\n              WebkitBackgroundClip: \"text\",\n              backgroundClip: \"text\",\n              color: \"transparent\",\n              paddingBottom: 0.14 * LABEL.size * s * k,\n            }}\n          >\n            {filled ? to : from} {noun}\n          </span>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n\nexport default CountGrid;\n",
      "type": "registry:component",
      "target": "components/snap-cn/count-grid.tsx"
    }
  ],
  "docs": "Docs and props: https://snapcn.dev/docs/scenes/count-grid?ref=cli — pass cards an array of image URLs and from/to/noun for the label. — 82 Pro components too — charts, launch scenes, device shots: https://snapcn.dev/docs/pricing?ref=cli-docs",
  "type": "registry:component"
}
