{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "treemap-split",
  "title": "Treemap Split",
  "description": "A treemap splits into category tiles, the biggest tile in gradient with a 38% label.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/treemap-split/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/SchibstedGrotesk\";\nimport type React from \"react\";\nimport { useId, useMemo } 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});\n\n/** gradients.sh \"fluted\" palette: deep blue, blue, orange, cream. The blue takes the accent. */\nconst FLUTED = [\"#1d2a8f\", \"#2f7bff\", \"#ff7a3d\", \"#ffe2c4\"] as const;\n\nfunction flutedFill(angle: number, accent: string, flute: number) {\n  return [\n    `repeating-linear-gradient(90deg, rgba(255,255,255,0.05) 0px, rgba(255,255,255,0) ${flute * 0.45}px, rgba(0,0,0,0.07) ${flute * 0.9}px, rgba(255,255,255,0.05) ${flute}px)`,\n    `linear-gradient(${angle}deg, ${FLUTED[0]} 0%, ${accent} 46%, ${FLUTED[2]} 84%, ${FLUTED[3]} 100%)`,\n  ].join(\", \");\n}\n\nexport interface TreemapItem {\n  label: string;\n  value: number;\n}\n\nexport interface TreemapSplitProps {\n  items?: TreemapItem[];\n  highlight?: number;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst DEFAULT_ITEMS: TreemapItem[] = [\n  { label: \"Captions\", value: 38 },\n  { label: \"Transitions\", value: 16 },\n  { label: \"Text\", value: 12 },\n  { label: \"Backgrounds\", value: 9 },\n  { label: \"Shaders\", value: 8 },\n  { label: \"Icons\", value: 6 },\n  { label: \"Charts\", value: 5 },\n  { label: \"UI\", value: 4 },\n  { label: \"Effects\", value: 2 },\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_SLIDE = Easing.bezier(0.7, 0, 0.2, 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    cell: \"#1A1A1D\",\n    edge: \"rgba(255,255,255,0.06)\",\n    hair: \"rgba(255,255,255,0.55)\",\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    cell: \"#E4E2DC\",\n    edge: \"rgba(18,18,20,0.05)\",\n    hair: \"rgba(18,18,20,0.5)\",\n    grain: 0.045,\n  },\n} as const;\n\nconst T0 = 12;\nconst STEP = 16;\nconst SPLIT = 22;\n\ninterface Rect {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n}\n\ninterface TNode {\n  id: string;\n  leaf: number | null;\n  depth: number;\n  vertical: boolean;\n  f: number;\n  start: number;\n  a: TNode | null;\n  b: TNode | null;\n}\n\nfunction worst(idx: number[], vals: number[], w: number, h: number): number {\n  if (idx.length === 1) {\n    const a = Math.max(w / Math.max(h, 1e-6), h / Math.max(w, 1e-6));\n    return vals[idx[0]] * Math.log(a) * (a > 2.2 ? 2 : 1);\n  }\n  const sum = idx.reduce((s, i) => s + vals[i], 0) || 1;\n  const vertical = w >= h;\n  let best = Number.POSITIVE_INFINITY;\n  let acc = 0;\n  for (let j = 0; j < idx.length - 1; j++) {\n    acc += vals[idx[j]];\n    const ff = acc / sum;\n    const s =\n      worst(\n        idx.slice(0, j + 1),\n        vals,\n        vertical ? w * ff : w,\n        vertical ? h : h * ff,\n      ) +\n      worst(\n        idx.slice(j + 1),\n        vals,\n        vertical ? w * (1 - ff) : w,\n        vertical ? h : h * (1 - ff),\n      );\n    if (s < best) best = s;\n  }\n  return best;\n}\n\nfunction build(\n  idx: number[],\n  vals: number[],\n  w: number,\n  h: number,\n  depth: number,\n  id: string,\n  stagger: number,\n): TNode {\n  const start = T0 + depth * STEP + stagger;\n  if (idx.length === 1) {\n    return {\n      id,\n      leaf: idx[0],\n      depth,\n      vertical: true,\n      f: 1,\n      start,\n      a: null,\n      b: null,\n    };\n  }\n  const sum = idx.reduce((s, i) => s + vals[i], 0) || 1;\n  const vertical = w >= h;\n  let k = 1;\n  let best = Number.POSITIVE_INFINITY;\n  let acc = 0;\n  for (let j = 0; j < idx.length - 1; j++) {\n    acc += vals[idx[j]];\n    const ff = acc / sum;\n    const s =\n      worst(\n        idx.slice(0, j + 1),\n        vals,\n        vertical ? w * ff : w,\n        vertical ? h : h * ff,\n      ) +\n      worst(\n        idx.slice(j + 1),\n        vals,\n        vertical ? w * (1 - ff) : w,\n        vertical ? h : h * (1 - ff),\n      );\n    if (s < best - 1e-6) {\n      best = s;\n      k = j + 1;\n    }\n  }\n  const left = idx.slice(0, k);\n  const right = idx.slice(k);\n  const f = left.reduce((s, i) => s + vals[i], 0) / sum;\n  const aw = vertical ? w * f : w;\n  const ah = vertical ? h : h * f;\n  const bw = vertical ? w - aw : w;\n  const bh = vertical ? h : h - ah;\n  return {\n    id,\n    leaf: null,\n    depth,\n    vertical,\n    f,\n    start,\n    a: build(left, vals, aw, ah, depth + 1, `${id}a`, stagger),\n    b: build(right, vals, bw, bh, depth + 1, `${id}b`, stagger + 2),\n  };\n}\n\ninterface Cell {\n  key: string;\n  rect: Rect;\n  leaf: number | null;\n  settle: number;\n}\n\ninterface Line {\n  key: string;\n  x1: number;\n  y1: number;\n  x2: number;\n  y2: number;\n  draw: number;\n  fade: number;\n}\n\nfunction lastEnd(n: TNode): number {\n  if (!n.a || !n.b) return 0;\n  return Math.max(n.start + SPLIT, lastEnd(n.a), lastEnd(n.b));\n}\n\nfunction layout(\n  n: TNode,\n  r: Rect,\n  t: number,\n  gap: number,\n  cells: Cell[],\n  lines: Line[],\n  settle: number,\n) {\n  if (!n.a || !n.b || t < n.start) {\n    cells.push({ key: n.id, rect: r, leaf: n.leaf, settle });\n    return;\n  }\n  const p = (t - n.start) / SPLIT;\n  const draw = interpolate(p, [0, 0.38], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const open = interpolate(p, [0.28, 1], [0, 1], {\n    ...CLAMP,\n    easing: EASE_SLIDE,\n  });\n  const f = 0.5 + (n.f - 0.5) * open;\n  const g = gap * open;\n  const end = n.start + SPLIT;\n  let ra: Rect;\n  let rb: Rect;\n  if (n.vertical) {\n    const cut = r.x + (r.w - g) * f;\n    ra = { x: r.x, y: r.y, w: cut - r.x, h: r.h };\n    rb = { x: cut + g, y: r.y, w: r.x + r.w - cut - g, h: r.h };\n    lines.push({\n      key: n.id,\n      x1: cut + g / 2,\n      y1: r.y,\n      x2: cut + g / 2,\n      y2: r.y + r.h,\n      draw,\n      fade: 1 - open,\n    });\n  } else {\n    const cut = r.y + (r.h - g) * f;\n    ra = { x: r.x, y: r.y, w: r.w, h: cut - r.y };\n    rb = { x: r.x, y: cut + g, w: r.w, h: r.y + r.h - cut - g };\n    lines.push({\n      key: n.id,\n      x1: r.x,\n      y1: cut + g / 2,\n      x2: r.x + r.w,\n      y2: cut + g / 2,\n      draw,\n      fade: 1 - open,\n    });\n  }\n  layout(n.a, ra, t, gap, cells, lines, end);\n  layout(n.b, rb, t, gap, cells, lines, end);\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={41}\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 TreemapSplit({\n  items = DEFAULT_ITEMS,\n  highlight,\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: TreemapSplitProps) {\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\n  const list = useMemo(\n    () =>\n      items\n        .map((it, i) => ({ ...it, i, value: Math.max(0, it.value) }))\n        .filter((it) => it.value > 0)\n        .sort((a, b) => b.value - a.value)\n        .slice(0, 10),\n    [items],\n  );\n  const total = list.reduce((s, x) => s + x.value, 0) || 1;\n  const hl = (() => {\n    if (highlight === undefined) return 0;\n    const at = list.findIndex((x) => x.i === highlight);\n    return at < 0 ? 0 : at;\n  })();\n\n  const mx = width * 0.07;\n  const my = height * 0.1;\n  const root: Rect = { x: mx, y: my, w: width - mx * 2, h: height - my * 2 };\n  const gap = 8 * u;\n  const radius = 6 * u;\n\n  const tree = useMemo(\n    () =>\n      build(\n        list.map((_, i) => i),\n        list.map((x) => x.value),\n        root.w,\n        root.h,\n        0,\n        \"n\",\n        0,\n      ),\n    [list, root.w, root.h],\n  );\n  const splitsDone = Math.max(lastEnd(tree), T0);\n  const ACC = splitsDone + 8;\n\n  const cells: Cell[] = [];\n  const lines: Line[] = [];\n  layout(tree, root, t, gap, cells, lines, 0);\n\n  const enter = interpolate(t, [0, 14], [0, 1], { ...CLAMP, easing: EASE_OUT });\n  const push = interpolate(t, [0, ACC + 50], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const scale = (0.94 + 0.06 * enter) * (0.985 + 0.015 * push);\n\n  const fill = interpolate(t, [ACC, ACC + 18], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const preFill = interpolate(t, [ACC - 8, ACC + 2], [0, 1], CLAMP);\n  const heroIn = interpolate(t, [ACC + 10, ACC + 30], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const heroLab = interpolate(t, [ACC + 18, ACC + 34], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const count = interpolate(t, [ACC + 10, ACC + 36], [0.55, 1], {\n    ...CLAMP,\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n  const dimOthers = interpolate(t, [ACC, ACC + 20], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          transform: `scale(${scale})`,\n          transformOrigin: \"50% 50%\",\n          opacity: enter,\n        }}\n      >\n        {cells.map((c) => {\n          const isHl = c.leaf === hl;\n          const item = c.leaf !== null ? list[c.leaf] : null;\n          const pct = item ? Math.round((item.value / total) * 100) : 0;\n          const roomy = c.rect.w >= 120 * u && c.rect.h >= 70 * u;\n          const labA =\n            item && roomy\n              ? interpolate(t, [c.settle - 4, c.settle + 12], [0, 1], {\n                  ...CLAMP,\n                  easing: EASE_OUT,\n                })\n              : 0;\n          const smallLab = isHl ? labA * (1 - preFill) : labA;\n          const heroFont = Math.min(\n            136 * u,\n            c.rect.h * 0.42,\n            c.rect.w / (String(pct).length * 0.58 + 0.75),\n          );\n          const cellStyle: React.CSSProperties = {\n            position: \"absolute\",\n            left: c.rect.x,\n            top: c.rect.y,\n            width: Math.max(0, c.rect.w),\n            height: Math.max(0, c.rect.h),\n            borderRadius: radius,\n            background: p.cell,\n            boxShadow: `inset 0 0 0 ${Math.max(1, u)}px ${p.edge}`,\n            overflow: \"hidden\",\n          };\n          return (\n            <div key={c.key} style={cellStyle}>\n              {isHl ? (\n                <div\n                  style={{\n                    position: \"absolute\",\n                    inset: 0,\n                    background: flutedFill(10 + 8 * push, accent, 26 * u),\n                    clipPath: `inset(0 ${(1 - fill) * 100}% 0 0)`,\n                  }}\n                />\n              ) : null}\n              {item ? (\n                <div\n                  style={{\n                    position: \"absolute\",\n                    left: 16 * u,\n                    top: 14 * u,\n                    display: \"flex\",\n                    flexDirection: \"column\",\n                    gap: 3 * u,\n                    opacity: smallLab * (isHl ? 1 : 1 - 0.35 * dimOthers),\n                    transform: `translateY(${(1 - labA) * 6 * u}px)`,\n                    whiteSpace: \"nowrap\",\n                  }}\n                >\n                  <span\n                    style={{\n                      fontFamily: sans,\n                      fontWeight: 500,\n                      fontSize: 18 * u,\n                      letterSpacing: \"-0.01em\",\n                      color: p.fg,\n                    }}\n                  >\n                    {item.label}\n                  </span>\n                  <span\n                    style={{\n                      fontFamily: sans,\n                      fontWeight: 400,\n                      fontSize: 15 * u,\n                      color: p.muted,\n                      fontVariantNumeric: \"tabular-nums\",\n                    }}\n                  >\n                    {pct}%\n                  </span>\n                </div>\n              ) : null}\n              {isHl && item ? (\n                <div\n                  style={{\n                    position: \"absolute\",\n                    left: 26 * u,\n                    bottom: 18 * u,\n                    display: \"flex\",\n                    flexDirection: \"column\",\n                    whiteSpace: \"nowrap\",\n                  }}\n                >\n                  <span\n                    style={{\n                      fontFamily: sans,\n                      fontWeight: 400,\n                      fontSize: 28 * u,\n                      letterSpacing: \"-0.015em\",\n                      color: \"rgba(255,255,255,0.92)\",\n                      opacity: heroLab,\n                      transform: `translateY(${(1 - heroLab) * 8 * u}px)`,\n                      marginLeft: 4 * u,\n                    }}\n                  >\n                    {item.label}\n                  </span>\n                  <span\n                    style={{\n                      fontFamily: sans,\n                      fontWeight: 500,\n                      fontSize: heroFont,\n                      lineHeight: 1,\n                      letterSpacing: \"-0.05em\",\n                      color: \"#FFFFFF\",\n                      fontVariantNumeric: \"tabular-nums\",\n                      opacity: heroIn,\n                      filter:\n                        heroIn < 0.99\n                          ? `blur(${(1 - heroIn) * 8 * u}px)`\n                          : undefined,\n                      transform: `translateY(${(1 - heroIn) * 20 * u}px)`,\n                    }}\n                  >\n                    {Math.round(pct * count)}\n                    <span\n                      style={{\n                        fontSize: heroFont * 0.55,\n                        letterSpacing: \"-0.03em\",\n                      }}\n                    >\n                      %\n                    </span>\n                  </span>\n                </div>\n              ) : null}\n            </div>\n          );\n        })}\n        <svg\n          width={width}\n          height={height}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          {lines.map((l) => {\n            if (l.draw <= 0 || l.fade <= 0) return null;\n            const x2 = l.x1 + (l.x2 - l.x1) * l.draw;\n            const y2 = l.y1 + (l.y2 - l.y1) * l.draw;\n            return (\n              <line\n                key={l.key}\n                x1={l.x1}\n                y1={l.y1}\n                x2={x2}\n                y2={y2}\n                stroke={p.hair}\n                strokeWidth={Math.max(1, 1.25 * u)}\n                opacity={l.fade}\n              />\n            );\n          })}\n        </svg>\n      </AbsoluteFill>\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 85% 85% at 50% 50%, transparent 65%, ${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/treemap-split.tsx"
    }
  ],
  "type": "registry:component"
}
