{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "module-count",
  "title": "Module Count",
  "description": "A pixel-block number snaps to 48 on a gridded teal ground with a short caption.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/module-count/index.tsx",
      "content": "\"use client\";\n\nimport { loadVariableFont } from \"@remotion/google-fonts/Archivo\";\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst archivo = loadVariableFont(\"normal\", { subsets: [\"latin\"] });\n\nexport interface ModuleCountProps {\n  value?: number;\n  from?: number;\n  prefix?: string;\n  suffix?: string;\n  label?: string;\n  steps?: number;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: {\n    bg: \"#0E4D4A\",\n    ink: \"#F4E9D8\",\n    accent: \"#FF6B4A\",\n    second: \"#FFC93C\",\n    grid: \"rgba(244,233,216,0.14)\",\n  },\n  light: {\n    bg: \"#F4E9D8\",\n    ink: \"#0E4D4A\",\n    accent: \"#FF6B4A\",\n    second: \"#0E4D4A\",\n    grid: \"rgba(14,77,74,0.16)\",\n  },\n} as const;\n\ntype Cell = \".\" | \"#\" | \"a\" | \"b\" | \"c\" | \"d\" | \"o\";\n\nconst GLYPHS: Record<string, string[]> = {\n  \"0\": [\"a#b\", \"#.#\", \"#.#\", \"#.#\", \"d#c\"],\n  \"1\": [\"a#.\", \".#.\", \".#.\", \".#.\", \"###\"],\n  \"2\": [\"a#b\", \"..#\", \"a#c\", \"#..\", \"###\"],\n  \"3\": [\"a#b\", \"..#\", \".##\", \"..#\", \"d#c\"],\n  \"4\": [\"#.#\", \"#.#\", \"d##\", \"..#\", \"..#\"],\n  \"5\": [\"###\", \"#..\", \"##b\", \"..#\", \"d#c\"],\n  \"6\": [\"a##\", \"#..\", \"##b\", \"#.#\", \"d#c\"],\n  \"7\": [\"###\", \"..#\", \".ac\", \".#.\", \".#.\"],\n  \"8\": [\"a#b\", \"#.#\", \"###\", \"#.#\", \"d#c\"],\n  \"9\": [\"a#b\", \"#.#\", \"d##\", \"..#\", \"##c\"],\n  \"+\": [\"...\", \".#.\", \"###\", \".#.\", \"...\"],\n  \"-\": [\"...\", \"...\", \"###\", \"...\", \"...\"],\n  \"×\": [\"...\", \"#.#\", \".#.\", \"#.#\", \"...\"],\n  x: [\"...\", \"#.#\", \".#.\", \"#.#\", \"...\"],\n  \"%\": [\"o.#\", \"..#\", \".#.\", \"#..\", \"#.o\"],\n  k: [\"#..\", \"#.a\", \"##c\", \"#.b\", \"#.#\"],\n  K: [\"#..\", \"#.a\", \"##c\", \"#.b\", \"#.#\"],\n  M: [\"##b##\", \"#.#.#\", \"#.#.#\", \"#...#\", \"#...#\"],\n  \".\": [\".\", \".\", \".\", \".\", \"o\"],\n  \",\": [\".\", \".\", \".\", \".\", \"c\"],\n  $: [\"a##\", \"#..\", \"d#b\", \"..#\", \"##c\"],\n};\n\nconst ROWS = 5;\nconst BLANK3 = [\"...\", \"...\", \"...\", \"...\", \"...\"];\n\nconst RADII: Record<Cell, [number, number, number, number]> = {\n  \".\": [0, 0, 0, 0],\n  \"#\": [0, 0, 0, 0],\n  a: [1, 0, 0, 0],\n  b: [0, 1, 0, 0],\n  c: [0, 0, 1, 0],\n  d: [0, 0, 0, 1],\n  o: [0.5, 0.5, 0.5, 0.5],\n};\n\nconst GAP = 0.09;\nconst GAP_LOCK = 0.045;\nconst FIT_W = 0.84;\nconst CAP_H = 0.46;\nconst TRACK_LABEL = -0.012;\n\nconst START = 10;\nconst FIRST_GAP = 4;\nconst LAST_GAP = 9;\nconst MORPH = 7;\nconst COL_DELAY = 0.7;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IN_OUT = Easing.bezier(0.65, 0, 0.35, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nfunction glyph(ch: string | undefined): string[] {\n  if (!ch) return BLANK3;\n  return GLYPHS[ch] ?? BLANK3;\n}\n\nfunction format(n: number): string {\n  return Math.round(n).toLocaleString(\"en-US\");\n}\n\nfunction blankLike(cols: number): string[] {\n  return Array.from({ length: ROWS }, () => \".\".repeat(cols));\n}\n\nexport function ModuleCount({\n  value = 48,\n  from = 0,\n  prefix = \"\",\n  suffix = \"\",\n  label = \"Scenes, every one snapped to a single grid\",\n  steps = 11,\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: ModuleCountProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const bg = background ?? c.bg;\n  const clear = bg === \"transparent\";\n\n  const plan = useMemo(() => {\n    const n = Math.max(1, Math.round(steps));\n    const values: number[] = [];\n    for (let k = 0; k <= n; k++) {\n      const p = k / n;\n      const eased = 1 - (1 - p) ** 2.4;\n      const v = Math.round(from + (value - from) * eased);\n      if (values.length === 0 || values[values.length - 1] !== v) {\n        values.push(v);\n      }\n    }\n    if (values[values.length - 1] !== Math.round(value)) {\n      values.push(Math.round(value));\n    }\n    const finalNum = format(value);\n    const finalStr = `${prefix}${finalNum}${suffix}`;\n    const slots = Array.from(finalStr);\n    const numStart = Array.from(prefix).length;\n    const numLen = Array.from(finalNum).length;\n    const strings = values.map((v, k) => {\n      const num = Array.from(format(v));\n      const arr: (string | undefined)[] = slots.map(() => undefined);\n      for (let i = 0; i < Array.from(prefix).length; i++) {\n        arr[i] = slots[i];\n      }\n      for (let j = 0; j < num.length && j < numLen; j++) {\n        arr[numStart + numLen - 1 - j] = num[num.length - 1 - j];\n      }\n      if (k === values.length - 1) {\n        for (let i = numStart + numLen; i < slots.length; i++) {\n          arr[i] = slots[i];\n        }\n      }\n      return arr;\n    });\n    const times: number[] = [];\n    let at = START;\n    const last = Math.max(1, values.length - 1);\n    for (let k = 0; k < values.length; k++) {\n      times.push(at);\n      const p = k / last;\n      at += FIRST_GAP + (LAST_GAP - FIRST_GAP) * p ** 1.6;\n    }\n    const cols = slots.map((ch) => glyph(ch)[0].length);\n    return { strings, times, slots, cols };\n  }, [value, from, prefix, suffix, steps]);\n\n  const { strings, times, slots, cols } = plan;\n  const lastStep = times[times.length - 1];\n\n  const unitsW =\n    cols.reduce((s, n) => s + n * (1 + GAP), 0) +\n    (1 + GAP) * Math.max(0, slots.length - 1) -\n    GAP;\n  const unitsH = ROWS + (ROWS - 1) * GAP;\n  const s = Math.min(\n    (W * (W < H ? 0.84 : FIT_W)) / unitsW,\n    (H * CAP_H) / unitsH,\n  );\n\n  const lock = interpolate(t, [lastStep + 4, lastStep + 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IN_OUT,\n  });\n  const gap = GAP + (GAP_LOCK - GAP) * lock;\n\n  const slotX: number[] = [];\n  let x = 0;\n  for (let i = 0; i < slots.length; i++) {\n    slotX.push(x);\n    x += (cols[i] + 1) * (1 + gap) * s;\n  }\n  const boxW = x - (1 + 2 * gap) * s;\n  const boxH = (ROWS + (ROWS - 1) * gap) * s;\n\n  const visibleFrom = (k: number) => {\n    const arr = strings[k];\n    let first = arr.findIndex((ch) => ch !== undefined);\n    if (first < 0) first = 0;\n    let lastI = arr.length - 1;\n    while (lastI > first && arr[lastI] === undefined) lastI--;\n    const l = slotX[first];\n    const r = slotX[lastI] + (cols[lastI] + (cols[lastI] - 1) * gap) * s;\n    return (l + r) / 2;\n  };\n  let centre = visibleFrom(0);\n  for (let k = 1; k < strings.length; k++) {\n    const p = interpolate(t, [times[k], times[k] + MORPH + 4], [0, 1], {\n      ...CLAMP,\n      easing: EASE_IN_OUT,\n    });\n    centre += (visibleFrom(k) - centre) * p;\n  }\n  const shiftX = boxW / 2 - centre;\n\n  const intro = interpolate(t, [START - 8, START + 6], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  const spot = accent ?? c.accent;\n  const isFigure = (ch: string) => /[0-9.,]/.test(ch);\n\n  const cells: React.ReactNode[] = [];\n  let colIndex = 0;\n  for (let i = 0; i < slots.length; i++) {\n    const n = cols[i];\n    for (let col = 0; col < n; col++) {\n      const sweep = colIndex * COL_DELAY;\n      colIndex++;\n      for (let row = 0; row < ROWS; row++) {\n        let k = 0;\n        for (let q = 1; q < strings.length; q++) {\n          if (t >= times[q] + sweep) k = q;\n        }\n        const codeAt = (q: number): Cell => {\n          const g =\n            strings[q][i] === undefined ? blankLike(n) : glyph(strings[q][i]);\n          return (g[row]?.[col] ?? \".\") as Cell;\n        };\n        const now = codeAt(k);\n        const prev = k > 0 ? codeAt(k - 1) : \".\";\n        const p =\n          k === 0\n            ? 1\n            : interpolate(\n                t,\n                [times[k] + sweep, times[k] + sweep + MORPH],\n                [0, 1],\n                {\n                  ...CLAMP,\n                  easing: EASE_OUT,\n                },\n              );\n        const onPrev = prev === \".\" ? 0 : 1;\n        const onNow = now === \".\" ? 0 : 1;\n        const pOn = onNow < onPrev ? Math.min(1, p * 2.6) : p;\n        let on = onPrev + (onNow - onPrev) * pOn;\n        if (k === 0) on = onNow * intro;\n        if (on <= 0.001) continue;\n        const rp = RADII[prev === \".\" ? now : prev];\n        const rn = RADII[now === \".\" ? prev : now];\n        const r = rp.map((v, j) => (v + (rn[j] - v) * p) * s);\n        const scale = 0.62 + 0.38 * on;\n        const left = slotX[i] + col * (1 + gap) * s;\n        const top = row * (1 + gap) * s;\n        const enterLift = k === 0 ? (1 - intro) * s * 0.4 : 0;\n        cells.push(\n          <div\n            key={`${i}-${col}-${row}`}\n            style={{\n              position: \"absolute\",\n              left,\n              top,\n              width: s,\n              height: s,\n              borderRadius: `${r[0]}px ${r[1]}px ${r[2]}px ${r[3]}px`,\n              background: isFigure(slots[i]) ? spot : c.second,\n              opacity: on,\n              transform: `translateY(${enterLift.toFixed(2)}px) scale(${scale.toFixed(4)})`,\n            }}\n          />,\n        );\n      }\n    }\n  }\n\n  const labelAt = lastStep - 6;\n  const P = (1 + gap) * s;\n  const boxTop = H / 2 - boxH / 2 - P * 0.5;\n  const boxLeft = W / 2 - boxW / 2 + shiftX;\n  const mod = (v: number) => ((v % P) + P) % P;\n  const gx0 = mod(boxLeft - (gap * s) / 2);\n  const gy0 = mod(boxTop - (gap * s) / 2);\n  const portrait = H > W * 1.1;\n  const M = Math.min(W, H) * 0.07;\n  const labelLeft = gx0 + Math.ceil((M - gx0) / P) * P;\n  const labelRows = 2;\n  const labelTop = gy0 + Math.floor((H - M - gy0) / P) * P - labelRows * P;\n  const labelSize = Math.min(\n    Math.max(P * 0.4, H * 0.036),\n    portrait ? W * 0.06 : H * 0.056,\n  );\n  const labelCols = Math.max(\n    4,\n    Math.floor((portrait ? W * 0.84 : W * 0.42) / P),\n  );\n  const words = label ? label.split(\" \") : [];\n  const gridLine = Math.max(1, s * 0.016);\n\n  return (\n    <AbsoluteFill style={{ background: bg, overflow: \"hidden\" }}>\n      {clear ? null : (\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            backgroundImage: `linear-gradient(to right, ${c.grid} ${gridLine}px, transparent ${gridLine}px), linear-gradient(to bottom, ${c.grid} ${gridLine}px, transparent ${gridLine}px)`,\n            backgroundSize: `${P}px ${P}px`,\n            backgroundPosition: `${gx0 - gridLine / 2}px ${gy0 - gridLine / 2}px`,\n            opacity: lock,\n          }}\n        />\n      )}\n      <div\n        style={{\n          position: \"absolute\",\n          left: boxLeft,\n          top: boxTop,\n          width: boxW,\n          height: boxH,\n        }}\n      >\n        {cells}\n      </div>\n      {label ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: labelLeft + P * 0.16,\n            top: labelTop,\n            height: labelRows * P,\n            display: \"flex\",\n            flexWrap: \"wrap\",\n            alignContent: \"flex-end\",\n            paddingBottom: P * 0.16,\n            width: labelCols * P - P * 0.32,\n            fontFamily: archivo.fontFamily,\n            fontVariationSettings: '\"wdth\" 125, \"wght\" 450',\n            fontSize: labelSize,\n            lineHeight: 1.18,\n            letterSpacing: `${TRACK_LABEL}em`,\n            color: c.ink,\n            textWrap: \"balance\",\n          }}\n        >\n          {words.map((w, k) => {\n            const p = interpolate(\n              t,\n              [labelAt + k * 3, labelAt + k * 3 + 16],\n              [0, 1],\n              { ...CLAMP, easing: EASE_OUT },\n            );\n            return (\n              <span\n                key={`${w}-${k}`}\n                style={{\n                  display: \"inline-block\",\n                  whiteSpace: \"pre\",\n                  opacity: p,\n                  transform: `translateY(${((1 - p) * labelSize * 0.6).toFixed(2)}px)`,\n                }}\n              >\n                {w}\n                {k < words.length - 1 ? \" \" : \"\"}\n              </span>\n            );\n          })}\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/module-count.tsx"
    }
  ],
  "type": "registry:component"
}
