{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-split",
  "title": "Number Split",
  "description": "A big 300+ splits down a bracket into four category counts on colored bars.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/number-split/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/DMSans\";\nimport { loadFont as loadDisplay } from \"@remotion/google-fonts/Fraunces\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: sans } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: display } = loadDisplay(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\nloadDisplay(\"italic\", { weights: [\"400\"], subsets: [\"latin\"] });\n\nexport interface SplitPart {\n  label: string;\n  value: number;\n}\n\nexport interface NumberSplitProps {\n  total?: string;\n  label?: string;\n  parts?: SplitPart[];\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst DEFAULT_PARTS: SplitPart[] = [\n  { label: \"animations\", value: 120 },\n  { label: \"captions\", value: 31 },\n  { label: \"transitions\", value: 90 },\n  { label: \"backgrounds\", value: 60 },\n];\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_LAND = Easing.bezier(0.2, 1.1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    key: \"rgba(255,255,255,0.05)\",\n    fg: \"#F3F2EF\",\n    muted: \"#8B8A90\",\n    hair: \"rgba(255,255,255,0.28)\",\n    // gradients.sh \"mesh\": accent blue through periwinkle to lilac.\n    ramp: [\"#2e45b7\", \"#8796ee\", \"#ecdcf2\"],\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    key: \"rgba(255,255,255,0.85)\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    hair: \"rgba(18,18,20,0.3)\",\n    ramp: [\"#2e45b7\", \"#6f7fe6\", \"#b3a3ec\"],\n    grain: 0.045,\n  },\n} as const;\n\nconst BAR_IN = 10;\nconst MOVE = 36;\nconst MOVE_DUR = 26;\nconst SPLIT = MOVE + MOVE_DUR - 10;\nconst TREE = MOVE + MOVE_DUR - 2;\n\nexport const numberSplitLength = () => TREE + 8 + 12 + 6 + 24 + 40;\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, pointerEvents: \"none\" }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={11}\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\nfunction widths(values: number[], total: number, min: number) {\n  const sum = values.reduce((a, b) => a + Math.max(0, b), 0) || 1;\n  let w = values.map((v) => (Math.max(0, v) / sum) * total);\n  for (let pass = 0; pass < 4; pass++) {\n    const small = w.map((x) => x < min);\n    const fixed = small.filter(Boolean).length * min;\n    const rest = w.reduce((a, x, i) => (small[i] ? a : a + x), 0);\n    if (!small.some(Boolean) || rest <= 0) break;\n    w = w.map((x, i) => (small[i] ? min : (x / rest) * (total - fixed)));\n  }\n  return w;\n}\n\nfunction splitUnit(total: string) {\n  const m = total.match(/^([\\d.,\\s]+)(.*)$/);\n  return m ? { num: m[1], unit: m[2] } : { num: total, unit: \"\" };\n}\n\nexport function NumberSplit({\n  total = \"300+\",\n  label = \"components\",\n  parts = DEFAULT_PARTS,\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: NumberSplitProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const u = Math.min(W / 1280, H / 720);\n  const c = THEMES[theme];\n  const n = Math.max(1, parts.length);\n\n  const L = W * 0.1;\n  const rowW = W - 2 * L;\n  const gap = 20 * u;\n  const segW = widths(\n    parts.map((p) => p.value),\n    rowW - gap * (n - 1),\n    120 * u,\n  );\n\n  const heroF0 = 240 * u;\n  const heroF1 = 156 * u;\n  const valF = 64 * u;\n  const barH = 16 * u;\n  const treeH = 64 * u;\n  const block = heroF1 * 0.92 + 18 * u + treeH + barH + 26 * u + valF + 34 * u;\n  const top = (H - block) / 2;\n  const heroTop1 = top;\n  const treeTop = top + heroF1 * 0.92 + 18 * u;\n  const barY1 = treeTop + treeH;\n  const valTop = barY1 + barH + 26 * u;\n\n  const heroTop0 = H / 2 - heroF0 * 0.56;\n  const barY0 = heroTop0 + heroF0 * 0.92 + 30 * u;\n  const { num, unit } = splitUnit(total);\n  const heroW0 = (num.length * 0.6 + unit.length * 0.4) * heroF0;\n\n  const heroIn = interpolate(t, [0, 18], [0, 1], {\n    ...CLAMP,\n    easing: EASE_LAND,\n  });\n  const heroBlur = interpolate(t, [0, 9], [1, 0], CLAMP);\n  const barIn = interpolate(t, [BAR_IN, BAR_IN + 20], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const mv = interpolate(t, [MOVE, MOVE + MOVE_DUR], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const split = interpolate(t, [SPLIT, SPLIT + 12], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const gapP = interpolate(t, [SPLIT, SPLIT + 16], [0, 1], {\n    ...CLAMP,\n    easing: EASE_LAND,\n  });\n  const labelIn = interpolate(t, [MOVE + 14, MOVE + 32], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const stemP = interpolate(t, [TREE, TREE + 8], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const spanP = interpolate(t, [TREE + 8, TREE + 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const cam = interpolate(t, [0, TREE + 90], [1, 1.025], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n\n  const heroScale = 1 + (heroF1 / heroF0 - 1) * mv;\n  const heroTop = heroTop0 + (heroTop1 - heroTop0) * mv;\n  const labelF = 44 * u;\n  const labelW = label.length * 0.43 * labelF;\n  const lockShift = -((labelW + 22 * u) / 2) * labelIn;\n\n  const barY = barY0 + (barY1 - barY0) * mv;\n  const fullW = heroW0 * 0.86 + (rowW - heroW0 * 0.86) * mv;\n  const scaleW = fullW / rowW;\n  const gapNow = gap * gapP;\n  const segs: { x: number; w: number; cx: number }[] = [];\n  const sumW = segW.reduce((a, b) => a + b, 0);\n  let x = W / 2 - (sumW * scaleW + gapNow * (n - 1)) / 2;\n  for (let i = 0; i < n; i++) {\n    const w = segW[i] * scaleW;\n    segs.push({ x, w, cx: x + w / 2 });\n    x += w + gapNow;\n  }\n\n  const stroke = Math.max(1, u);\n  const stemTop = treeTop;\n  const midY = treeTop + treeH * 0.45;\n  const firstCx = segs[0].cx;\n  const lastCx = segs[n - 1].cx;\n  const center = W / 2;\n  const root = center + lockShift;\n  const dropAt = segs.map((s) => {\n    const end = s.cx < root ? firstCx : lastCx;\n    const frac = Math.min(\n      1,\n      Math.abs(s.cx - root) / Math.max(1, Math.abs(end - root)),\n    );\n    return TREE + 8 + 12 * frac;\n  });\n\n  const gradId = `split${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const barL = center + (segs[0].x - center) * barIn;\n  const barR = center + (segs[n - 1].x + segs[n - 1].w - center) * barIn;\n  const drift = interpolate(t, [0, 150], [0, 1], CLAMP);\n\n  return (\n    <AbsoluteFill style={{ background: c.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 60% 65% at 50% 42%, ${c.key}, transparent 72%)`,\n        }}\n      />\n      <AbsoluteFill\n        style={{ transform: `scale(${cam})`, transformOrigin: \"50% 45%\" }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            top: heroTop,\n            display: \"flex\",\n            justifyContent: \"center\",\n            transform: `translateX(${lockShift}px)`,\n          }}\n        >\n          <div\n            style={{\n              position: \"relative\",\n              transformOrigin: \"50% 0\",\n              transform: `scale(${heroScale})`,\n            }}\n          >\n            <div\n              style={{\n                overflow: \"hidden\",\n                padding: `${heroF0 * 0.04}px ${heroF0 * 0.04}px 0`,\n                margin: `${-heroF0 * 0.04}px ${-heroF0 * 0.04}px 0`,\n              }}\n            >\n              <div\n                style={{\n                  display: \"flex\",\n                  alignItems: \"flex-start\",\n                  fontFamily: display,\n                  fontWeight: 400,\n                  fontSize: heroF0,\n                  lineHeight: 0.92,\n                  letterSpacing: \"-0.035em\",\n                  fontVariantNumeric: \"tabular-nums\",\n                  color: c.fg,\n                  transform: `translateY(${(1 - heroIn) * 104}%)`,\n                  filter:\n                    heroBlur > 0.02\n                      ? `blur(${heroBlur * 10 * u}px)`\n                      : undefined,\n                }}\n              >\n                <span>{num}</span>\n                {unit ? (\n                  <span\n                    style={{\n                      color: accent,\n                      fontSize: heroF0 * 0.56,\n                      lineHeight: 1,\n                      marginLeft: heroF0 * 0.03,\n                      marginTop: heroF0 * 0.04,\n                    }}\n                  >\n                    {unit}\n                  </span>\n                ) : null}\n              </div>\n            </div>\n            <div\n              style={{\n                position: \"absolute\",\n                left: `calc(100% + ${(22 * u * heroF0) / heroF1}px)`,\n                bottom: heroF0 * 0.06,\n                fontFamily: display,\n                fontStyle: \"italic\",\n                fontWeight: 400,\n                fontSize: labelF / (heroF1 / heroF0),\n                lineHeight: 1,\n                letterSpacing: \"-0.01em\",\n                whiteSpace: \"nowrap\",\n                color: c.muted,\n                opacity: labelIn,\n                transform: `translateX(${(1 - labelIn) * -14 * u}px)`,\n                filter: `blur(${(1 - labelIn) * 6 * u}px)`,\n              }}\n            >\n              {label}\n            </div>\n          </div>\n        </div>\n\n        <svg\n          width={W}\n          height={H}\n          viewBox={`0 0 ${W} ${H}`}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          <defs>\n            <linearGradient\n              id={gradId}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={barL - (barR - barL) * 0.04 * drift}\n              y1={0}\n              x2={Math.max(barL + 1, barR)}\n              y2={0}\n            >\n              <stop offset={0} stopColor={accent} />\n              <stop offset={0.2} stopColor={c.ramp[0]} />\n              <stop offset={0.62} stopColor={c.ramp[1]} />\n              <stop offset={1} stopColor={c.ramp[2]} />\n            </linearGradient>\n          </defs>\n          <line\n            x1={root}\n            y1={stemTop}\n            x2={root}\n            y2={stemTop + (midY - stemTop) * stemP}\n            stroke={c.hair}\n            strokeWidth={stroke}\n          />\n          <line\n            x1={root + (firstCx - root) * spanP}\n            y1={midY}\n            x2={root + (lastCx - root) * spanP}\n            y2={midY}\n            stroke={c.hair}\n            strokeWidth={stroke}\n            opacity={stemP >= 1 ? 1 : 0}\n          />\n          {segs.map((s, i) => {\n            const ds = dropAt[i];\n            const drop = interpolate(t, [ds, ds + 10], [0, 1], {\n              ...CLAMP,\n              easing: EASE_OUT,\n            });\n            const dp = drop > 0 ? 1 : 0;\n            return (\n              <g key={i} opacity={dp}>\n                <line\n                  x1={s.cx}\n                  y1={midY}\n                  x2={s.cx}\n                  y2={midY + (barY1 - 8 * u - midY) * drop}\n                  stroke={c.hair}\n                  strokeWidth={stroke}\n                />\n              </g>\n            );\n          })}\n          {split < 1 ? (\n            <rect\n              x={center + (segs[0].x - center) * barIn}\n              y={barY}\n              width={Math.max(\n                0,\n                (segs[n - 1].x + segs[n - 1].w - segs[0].x) * barIn,\n              )}\n              height={barH}\n              rx={barH / 2}\n              fill={`url(#${gradId})`}\n              opacity={interpolate(split, [0, 0.35], [1, 0], CLAMP)}\n            />\n          ) : null}\n          {split > 0\n            ? segs.map((s, i) => {\n                const w = s.w * barIn;\n                const cx = center + (s.x - center) * barIn;\n                return (\n                  <rect\n                    key={i}\n                    x={cx}\n                    y={barY}\n                    width={Math.max(0, w)}\n                    height={barH}\n                    rx={barH / 2}\n                    fill={`url(#${gradId})`}\n                  />\n                );\n              })\n            : null}\n        </svg>\n\n        {parts.map((p, i) => {\n          const s = segs[i];\n          const start = dropAt[i] + 6;\n          const e = interpolate(t, [start, start + 16], [0, 1], {\n            ...CLAMP,\n            easing: EASE_LAND,\n          });\n          const count = interpolate(t, [start, start + 24], [0, 1], {\n            ...CLAMP,\n            easing: EASE_OUT,\n          });\n          const lab = interpolate(t, [start + 4, start + 18], [0, 1], {\n            ...CLAMP,\n            easing: EASE_OUT,\n          });\n          return (\n            <div\n              key={i}\n              style={{\n                position: \"absolute\",\n                left: s.cx - 150 * u,\n                width: 300 * u,\n                top: valTop,\n                display: \"flex\",\n                flexDirection: \"column\",\n                alignItems: \"center\",\n              }}\n            >\n              <div style={{ overflow: \"hidden\", paddingBottom: 2 * u }}>\n                <div\n                  style={{\n                    fontFamily: display,\n                    fontWeight: 400,\n                    fontSize: valF,\n                    lineHeight: 1,\n                    letterSpacing: \"-0.03em\",\n                    fontVariantNumeric: \"tabular-nums\",\n                    color: i === 0 ? accent : c.fg,\n                    transform: `translateY(${(1 - e) * 108}%)`,\n                  }}\n                >\n                  {Math.round(p.value * count)}\n                </div>\n              </div>\n              <div\n                style={{\n                  marginTop: 10 * u,\n                  fontFamily: sans,\n                  fontWeight: 400,\n                  fontSize: 19 * u,\n                  lineHeight: 1,\n                  letterSpacing: \"0\",\n                  color: c.muted,\n                  whiteSpace: \"nowrap\",\n                  opacity: lab,\n                  transform: `translateY(${(1 - lab) * 8 * u}px)`,\n                }}\n              >\n                {p.label}\n              </div>\n            </div>\n          );\n        })}\n      </AbsoluteFill>\n      <Grain opacity={c.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/number-split.tsx"
    }
  ],
  "type": "registry:component"
}
