{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "token-budget",
  "type": "registry:component",
  "title": "Token Budget",
  "description": "See exactly what's filling your model's context, as a field of cells or a treemap of the same parts; hover anything to light it in both, drop a file you don't need, and Compact folds older turns into a short summary.",
  "categories": [
    "component",
    "Mission Control"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json",
    "https://coss.com/ui/r/segmented-control.json"
  ],
  "files": [
    {
      "path": "registry/devl/token-budget.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { FileTextIcon, Grid3x3Icon, LayoutDashboardIcon, ShrinkIcon, XIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ui/segmented-control\";\nimport { cn } from \"@/lib/utils\";\n\nfunction useReducedMotionAfterMount() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && Boolean(prefers);\n}\n\nexport type BudgetPart = \"system\" | \"tools\" | \"memory\" | \"files\" | \"summary\" | \"chat\" | \"results\" | \"free\";\nexport type BudgetView = \"cells\" | \"treemap\";\n\nexport interface BudgetFile {\n  id: string;\n  name: string;\n  tokens: number;\n}\n\nexport interface TokenBudgetProps {\n  model?: string;\n  limit?: number;\n  system?: number;\n  tools?: number;\n  memory?: number;\n  files?: BudgetFile[];\n  chat?: number;\n  turns?: number;\n  results?: number;\n  keepRecent?: number;\n  summaryRatio?: number;\n  defaultView?: BudgetView;\n  cells?: number;\n  columns?: number;\n  onRemoveFile?: (id: string) => void;\n  onCompact?: (summary: { turns: number; summaryTokens: number; freed: number }) => void;\n  className?: string;\n}\n\nconst parts: Record<BudgetPart, { label: string; cell: string; tile: string; dot: string }> = {\n  system: {\n    label: \"System\",\n    cell: \"bg-stone-500 dark:bg-stone-400\",\n    tile: \"bg-stone-500/12 border-stone-500/30 text-stone-800 dark:bg-stone-400/14 dark:border-stone-400/30 dark:text-stone-200\",\n    dot: \"bg-stone-500 dark:bg-stone-400\",\n  },\n  tools: {\n    label: \"Tools\",\n    cell: \"bg-sky-500 dark:bg-sky-400\",\n    tile: \"bg-sky-500/12 border-sky-500/30 text-sky-800 dark:bg-sky-400/14 dark:border-sky-400/30 dark:text-sky-200\",\n    dot: \"bg-sky-500 dark:bg-sky-400\",\n  },\n  memory: {\n    label: \"Memory\",\n    cell: \"bg-emerald-500 dark:bg-emerald-400\",\n    tile: \"bg-emerald-500/12 border-emerald-500/30 text-emerald-800 dark:bg-emerald-400/14 dark:border-emerald-400/30 dark:text-emerald-200\",\n    dot: \"bg-emerald-500 dark:bg-emerald-400\",\n  },\n  files: {\n    label: \"Files\",\n    cell: \"bg-amber-500 dark:bg-amber-400\",\n    tile: \"bg-amber-500/14 border-amber-500/35 text-amber-900 dark:bg-amber-400/14 dark:border-amber-400/30 dark:text-amber-200\",\n    dot: \"bg-amber-500 dark:bg-amber-400\",\n  },\n  summary: {\n    label: \"Summary\",\n    cell: \"bg-lime-500 dark:bg-lime-400\",\n    tile: \"bg-lime-500/16 border-lime-600/40 text-lime-900 dark:bg-lime-400/16 dark:border-lime-400/35 dark:text-lime-200\",\n    dot: \"bg-lime-500 dark:bg-lime-400\",\n  },\n  chat: {\n    label: \"Chat\",\n    cell: \"bg-violet-500 dark:bg-violet-400\",\n    tile: \"bg-violet-500/12 border-violet-500/30 text-violet-800 dark:bg-violet-400/14 dark:border-violet-400/30 dark:text-violet-200\",\n    dot: \"bg-violet-500 dark:bg-violet-400\",\n  },\n  results: {\n    label: \"Tool results\",\n    cell: \"bg-rose-500 dark:bg-rose-400\",\n    tile: \"bg-rose-500/12 border-rose-500/30 text-rose-800 dark:bg-rose-400/14 dark:border-rose-400/30 dark:text-rose-200\",\n    dot: \"bg-rose-500 dark:bg-rose-400\",\n  },\n  free: {\n    label: \"Free\",\n    cell: \"bg-foreground/[0.07]\",\n    tile: \"border-dashed border-foreground/20 text-muted-foreground\",\n    dot: \"bg-foreground/15\",\n  },\n};\n\ninterface Segment {\n  id: string;\n  part: BudgetPart;\n  label: string;\n  tokens: number;\n}\n\ninterface Rect {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n}\n\nfunction orderedTreemap<T extends { tokens: number }>(items: T[], r: Rect, aspect: number, out: Map<T, Rect>) {\n  if (!items.length) return;\n  if (items.length === 1) {\n    out.set(items[0], r);\n    return;\n  }\n  const total = items.reduce((s, i) => s + i.tokens, 0) || 1;\n  let acc = 0;\n  let k = 1;\n  let best = Number.POSITIVE_INFINITY;\n  for (let i = 0; i < items.length - 1; i++) {\n    acc += items[i].tokens;\n    const d = Math.abs(acc - total / 2);\n    if (d < best) {\n      best = d;\n      k = i + 1;\n    }\n  }\n  const a = items.slice(0, k);\n  const ratio = a.reduce((s, i) => s + i.tokens, 0) / total;\n  if (r.w * aspect >= r.h) {\n    orderedTreemap(a, { ...r, w: r.w * ratio }, aspect, out);\n    orderedTreemap(items.slice(k), { ...r, x: r.x + r.w * ratio, w: r.w * (1 - ratio) }, aspect, out);\n  } else {\n    orderedTreemap(a, { ...r, h: r.h * ratio }, aspect, out);\n    orderedTreemap(items.slice(k), { ...r, y: r.y + r.h * ratio, h: r.h * (1 - ratio) }, aspect, out);\n  }\n}\n\nconst k = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(n >= 100_000 ? 0 : 1).replace(/\\.0$/, \"\")}k` : `${Math.round(n)}`);\n\nconst DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];\n\nfunction Roll({ text, className }: { text: string; className?: string }) {\n  const reduce = useReducedMotionAfterMount();\n  let d = 0;\n  let o = 0;\n  const glyphs = [...text]\n    .reverse()\n    .map((c) => ({ c, digit: c >= \"0\" && c <= \"9\", key: c >= \"0\" && c <= \"9\" ? `d${d++}` : `s${o++}` }))\n    .reverse();\n  return (\n    <span className={cn(\"inline-flex whitespace-nowrap tabular-nums leading-none\", className)}>\n      <span className=\"sr-only\">{text}</span>\n      <span aria-hidden className=\"inline-flex [--row:1.2em]\">\n        {glyphs.map(({ c, digit, key }) =>\n          digit ? (\n            <span className=\"relative inline-block h-(--row) overflow-hidden\" key={key}>\n              <span className=\"invisible block leading-(--row)\">0</span>\n              <motion.span\n                animate={{ y: `${-Number(c) * 10}%` }}\n                className=\"absolute inset-x-0 top-0 flex flex-col items-center\"\n                initial={false}\n                transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 260, damping: 30 }}\n              >\n                {DIGITS.map((n) => (\n                  <span className=\"block h-(--row) leading-(--row)\" key={n}>\n                    {n}\n                  </span>\n                ))}\n              </motion.span>\n            </span>\n          ) : (\n            <span className=\"inline-block h-(--row) leading-(--row)\" key={key}>\n              {c === \" \" ? \"\\u00a0\" : c}\n            </span>\n          ),\n        )}\n      </span>\n    </span>\n  );\n}\n\ntype Hover = { part: BudgetPart; id?: string } | null;\n\nexport function TokenBudget({\n  model = \"orbit-2\",\n  limit = 200_000,\n  system = 4_200,\n  tools = 14_800,\n  memory = 3_100,\n  files: initialFiles = [],\n  chat: initialChat = 0,\n  turns: initialTurns = 0,\n  results = 0,\n  keepRecent = 0.2,\n  summaryRatio = 0.06,\n  defaultView = \"cells\",\n  cells = 200,\n  columns = 20,\n  onRemoveFile,\n  onCompact,\n  className,\n}: TokenBudgetProps) {\n  const reduce = useReducedMotionAfterMount();\n  const [view, setView] = useState<BudgetView>(defaultView);\n  const [files, setFiles] = useState(initialFiles);\n  const [chat, setChat] = useState(initialChat);\n  const [turns, setTurns] = useState(initialTurns);\n  const [summary, setSummary] = useState(0);\n  const [folded, setFolded] = useState(0);\n  const [compacting, setCompacting] = useState(false);\n  const [hover, setHover] = useState<Hover>(null);\n  const [size, setSize] = useState({ w: 400, h: 200 });\n  const box = useRef<HTMLDivElement>(null);\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  useEffect(() => () => clearTimeout(timer.current), []);\n\n  useLayoutEffect(() => {\n    const el = box.current;\n    if (!el) return;\n    const ro = new ResizeObserver(() => setSize({ w: el.clientWidth, h: el.clientHeight }));\n    ro.observe(el);\n    setSize({ w: el.clientWidth, h: el.clientHeight });\n    return () => ro.disconnect();\n  }, []);\n\n  const segments = (\n    [\n      { id: \"system\", part: \"system\", label: \"System\", tokens: system },\n      { id: \"tools\", part: \"tools\", label: \"Tools\", tokens: tools },\n      { id: \"memory\", part: \"memory\", label: \"Memory\", tokens: memory },\n    ...files.map((f) => ({ id: f.id, part: \"files\" as const, label: f.name.split(\"/\").pop() ?? f.name, tokens: f.tokens })),\n    ...(summary > 0 ? [{ id: \"summary\", part: \"summary\" as const, label: \"Summary\", tokens: summary }] : []),\n    { id: \"chat\", part: \"chat\", label: \"Chat\", tokens: chat },\n      { id: \"results\", part: \"results\", label: \"Tool results\", tokens: results },\n    ] as Segment[]\n  ).filter((s) => s.tokens > 0);\n  const used = segments.reduce((s, x) => s + x.tokens, 0);\n  const free = Math.max(limit - used, 0);\n  const pct = Math.min(used / limit, 1);\n  const perCell = limit / cells;\n\n  const totals: Record<BudgetPart, number> = {\n    system,\n    tools,\n    memory,\n    files: files.reduce((s, f) => s + f.tokens, 0),\n    summary,\n    chat,\n    results,\n    free,\n  };\n\n  const owners: (Segment | null)[] = [];\n  let si = 0;\n  let end = segments[0]?.tokens ?? 0;\n  for (let i = 0; i < cells; i++) {\n    const mid = (i + 0.5) * perCell;\n    while (si < segments.length && mid > end) {\n      si++;\n      end += segments[si]?.tokens ?? 0;\n    }\n    owners.push(si < segments.length ? segments[si] : null);\n  }\n\n  const tiles = new Map<Segment, Rect>();\n  const all: Segment[] = [...segments, ...(free > 0 ? [{ id: \"free\", part: \"free\" as const, label: \"Free\", tokens: free }] : [])];\n  orderedTreemap(all, { x: 0, y: 0, w: 1, h: 1 }, size.w / Math.max(size.h, 1), tiles);\n\n  const lit = (seg: Segment | null) => {\n    if (!hover) return true;\n    const part = seg?.part ?? \"free\";\n    if (hover.id) return seg?.id === hover.id;\n    return part === hover.part;\n  };\n\n  const removeFile = (id: string) => {\n    setFiles((fs) => fs.filter((f) => f.id !== id));\n    setHover(null);\n    onRemoveFile?.(id);\n  };\n\n  const keptTurns = Math.max(1, Math.round(turns * keepRecent));\n  const foldTurns = turns - keptTurns;\n  const foldTokens = Math.round(chat * (1 - keepRecent));\n  const summaryTokens = Math.round(foldTokens * summaryRatio);\n  const canCompact = foldTurns >= 2 && !compacting;\n\n  const compact = () => {\n    if (!canCompact) return;\n    const apply = () => {\n      setChat((c) => c - foldTokens);\n      setSummary((s) => s + summaryTokens);\n      setTurns(keptTurns);\n      setFolded((f) => f + foldTurns);\n      setCompacting(false);\n      onCompact?.({ turns: foldTurns, summaryTokens, freed: foldTokens - summaryTokens });\n    };\n    if (reduce) return apply();\n    setCompacting(true);\n    setHover(null);\n    timer.current = setTimeout(apply, 900);\n  };\n\n  const focus = hover\n    ? hover.id\n      ? (() => {\n          const f = files.find((x) => x.id === hover.id);\n          return f ? { part: \"files\" as const, label: f.name, tokens: f.tokens } : null;\n        })()\n      : { part: hover.part, label: parts[hover.part].label, tokens: totals[hover.part] }\n    : null;\n\n  const legend: BudgetPart[] = [\"system\", \"tools\", \"memory\", \"files\", ...(summary > 0 ? ([\"summary\"] as const) : []), \"chat\", \"results\", \"free\"];\n  const tone = pct > 0.85 ? \"text-destructive-foreground\" : pct > 0.7 ? \"text-warning-foreground\" : \"text-foreground\";\n\n  return (\n    <div className={cn(\"flex w-[440px] max-w-full flex-col gap-4 rounded-2xl border bg-card p-4 text-card-foreground shadow-xs/5\", className)}>\n      <div className=\"flex items-start justify-between gap-3\">\n        <div className=\"flex min-w-0 flex-col gap-0.5\">\n          <span className=\"font-medium text-sm\">Context budget</span>\n          <span className=\"truncate font-mono text-muted-foreground text-xs\">{model}</span>\n        </div>\n        <SegmentedControl aria-label=\"View\" size=\"sm\">\n          {(\n            [\n              [\"cells\", \"Cells\", Grid3x3Icon],\n              [\"treemap\", \"Treemap\", LayoutDashboardIcon],\n            ] as const\n          ).map(([id, label, Icon]) => (\n            <SegmentedControlItem\n              aria-pressed={view === id}\n              className=\"text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n              data-pressed={view === id ? \"\" : undefined}\n              key={id}\n              onClick={() => setView(id)}\n            >\n              <Icon />\n              <span className=\"max-[420px]:sr-only\">{label}</span>\n            </SegmentedControlItem>\n          ))}\n        </SegmentedControl>\n      </div>\n\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <span className=\"flex items-baseline gap-1.5 text-sm\">\n          <Roll className={cn(\"font-medium text-2xl tracking-tight\", tone)} text={k(used)} />\n          <span className=\"text-muted-foreground text-xs\">of {k(limit)} tokens</span>\n        </span>\n        {focus ? (\n          <span className=\"flex min-w-0 items-center gap-1.5 text-xs\" role=\"status\">\n            <span className={cn(\"size-2 shrink-0 rounded-[calc(var(--radius)*0.2)]\", parts[focus.part].dot)} />\n            <span className=\"min-w-0 truncate font-medium\">{focus.label}</span>\n            <span className=\"shrink-0 text-muted-foreground tabular-nums\">\n              {k(focus.tokens)} · {Math.round((focus.tokens / limit) * 100)}%\n            </span>\n          </span>\n        ) : (\n          <Roll className={cn(\"text-xs\", pct > 0.7 ? tone : \"text-muted-foreground\")} text={`${Math.round(pct * 100)}% full`} />\n        )}\n      </div>\n\n      <div className=\"relative\" onPointerLeave={() => setHover(null)} ref={box}>\n        <div\n          aria-hidden={view !== \"cells\"}\n          className={cn(\n            \"grid gap-[3px] transition-[opacity,filter] duration-200 ease-out\",\n            view === \"cells\" ? \"opacity-100 blur-0\" : \"pointer-events-none opacity-0 blur-[3px]\",\n          )}\n          role=\"img\"\n          aria-label={`${k(used)} of ${k(limit)} tokens used`}\n          style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}\n        >\n          {owners.map((seg, i) => {\n            const row = Math.floor(i / columns);\n            const col = i % columns;\n            const squeeze = compacting && seg?.part === \"chat\";\n            const on = lit(seg);\n            const alt = seg?.part === \"files\" && files.findIndex((f) => f.id === seg.id) % 2 === 1;\n            return (\n              <motion.div\n                animate={\n                  squeeze\n                    ? { scaleX: 0.35, opacity: 0.55 }\n                    : { scaleX: 1, opacity: on ? (alt && !hover ? 0.7 : 1) : 0.18 }\n                }\n                className={cn(\"aspect-square rounded-[calc(var(--radius)*0.3)]\", seg ? parts[seg.part].cell : parts.free.cell)}\n                initial={false}\n                key={i}\n                onPointerEnter={() => setHover(seg ? { part: seg.part, id: seg.part === \"files\" ? seg.id : undefined } : { part: \"free\" })}\n                style={{ transition: reduce ? undefined : `background-color 320ms ease ${(row + col) * 8}ms` }}\n                transition={\n                  squeeze\n                    ? { duration: 0.5, delay: (col % 20) * 0.015, ease: [0.23, 1, 0.32, 1] }\n                    : reduce\n                      ? { duration: 0 }\n                      : { type: \"spring\", stiffness: 420, damping: 30 }\n                }\n              />\n            );\n          })}\n        </div>\n\n        <div\n          aria-hidden={view !== \"treemap\"}\n          className={cn(\n            \"absolute inset-0 transition-[opacity,filter] duration-200 ease-out\",\n            view === \"treemap\" ? \"opacity-100 blur-0\" : \"pointer-events-none opacity-0 blur-[3px]\",\n          )}\n        >\n          {all.map((seg) => {\n            const r = tiles.get(seg);\n            if (!r) return null;\n            const wPx = r.w * size.w;\n            const hPx = r.h * size.h;\n            const on = lit(seg.part === \"free\" ? null : seg);\n            const squeeze = compacting && seg.part === \"chat\";\n            return (\n              <motion.div\n                animate={{\n                  left: `${r.x * 100}%`,\n                  top: `${r.y * 100}%`,\n                  width: `${r.w * 100}%`,\n                  height: `${r.h * 100}%`,\n                  opacity: on ? 1 : 0.3,\n                }}\n                className=\"absolute p-[1.5px]\"\n                initial={false}\n                key={seg.id}\n                onPointerEnter={() => setHover({ part: seg.part, id: seg.part === \"files\" ? seg.id : undefined })}\n                transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 300, damping: 32 }}\n              >\n                <motion.div\n                  animate={squeeze ? { scaleX: 0.82, opacity: 0.6 } : { scaleX: 1, opacity: 1 }}\n                  className={cn(\"flex size-full flex-col justify-between overflow-hidden rounded-lg border p-2 text-xs\", parts[seg.part].tile)}\n                  initial={false}\n                  style={{ transformOrigin: \"0% 50%\" }}\n                  transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}\n                >\n                  {wPx > 56 && hPx > 26 && <span className=\"truncate font-medium leading-none\">{seg.label}</span>}\n                  {wPx > 40 && hPx > 46 && <span className=\"truncate tabular-nums leading-none opacity-75\">{k(seg.tokens)}</span>}\n                </motion.div>\n              </motion.div>\n            );\n          })}\n        </div>\n      </div>\n\n      <ul className=\"flex flex-wrap gap-1\">\n        {legend.map((part) => (\n          <li key={part}>\n            <button\n              className={cn(\n                \"flex h-7 items-center gap-1.5 rounded-lg px-2 text-xs outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring\",\n                hover?.part === part && !hover.id && \"bg-accent\",\n              )}\n              onBlur={() => setHover(null)}\n              onFocus={() => setHover({ part })}\n              onPointerEnter={() => setHover({ part })}\n              onPointerLeave={() => setHover(null)}\n              type=\"button\"\n            >\n              <span className={cn(\"size-2 rounded-[calc(var(--radius)*0.2)]\", parts[part].dot)} />\n              <span className=\"text-muted-foreground\">{parts[part].label}</span>\n              <Roll className=\"font-medium\" text={k(totals[part])} />\n            </button>\n          </li>\n        ))}\n      </ul>\n\n      {files.length > 0 && (\n        <ul className=\"-mt-1 flex flex-col\">\n          <AnimatePresence initial={false}>\n            {files.map((file) => (\n              <motion.li\n                animate={{ opacity: 1, height: \"auto\" }}\n                className=\"overflow-hidden\"\n                exit={{ opacity: 0, height: 0 }}\n                initial={{ opacity: 0, height: 0 }}\n                key={file.id}\n                transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 380, damping: 34 }}\n              >\n                <div\n                  className={cn(\"group flex h-8 items-center gap-2 rounded-lg pr-1 pl-2 transition-colors\", hover?.id === file.id && \"bg-accent\")}\n                  onPointerEnter={() => setHover({ part: \"files\", id: file.id })}\n                  onPointerLeave={() => setHover(null)}\n                >\n                  <FileTextIcon className=\"size-3.5 shrink-0 text-amber-600 dark:text-amber-400\" />\n                  <span className=\"min-w-0 flex-1 truncate font-mono text-xs\">{file.name}</span>\n                  <span className=\"text-muted-foreground text-xs tabular-nums\">{k(file.tokens)}</span>\n                  <Button\n                    aria-label={`Remove ${file.name} from context`}\n                    className=\"text-muted-foreground\"\n                    onBlur={() => setHover(null)}\n                    onClick={() => removeFile(file.id)}\n                    onFocus={() => setHover({ part: \"files\", id: file.id })}\n                    size=\"icon-xs\"\n                    variant=\"ghost\"\n                  >\n                    <XIcon />\n                  </Button>\n                </div>\n              </motion.li>\n            ))}\n          </AnimatePresence>\n        </ul>\n      )}\n\n      <div className=\"flex items-center justify-between gap-3 border-t pt-3\">\n        <span aria-live=\"polite\" className=\"min-w-0 text-pretty text-muted-foreground text-xs\">\n          {compacting\n            ? `Folding ${foldTurns} older turns into a summary…`\n            : canCompact\n              ? `${foldTurns} older turns could fold into ~${k(summaryTokens)}, freeing ${k(foldTokens - summaryTokens)}`\n              : folded > 0\n                ? `${folded} turns live in a ${k(summary)} summary; the last ${turns} stay word for word`\n                : \"Nothing to compact yet\"}\n        </span>\n        <Button className=\"shrink-0\" disabled={!canCompact} loading={compacting} onClick={compact} size=\"sm\" variant=\"outline\">\n          <ShrinkIcon />\n          Compact\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}