{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "branch-usage-table",
  "title": "BranchUsageTable",
  "description": "Per-branch consumption attribution: sortable metric columns, magnitude rules, an expandable tail, and a list layout for a single metric.",
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/neon-tokens.json",
    "https://ui.neon.com/r/skeleton.json"
  ],
  "files": [
    {
      "path": "src/components/branch-usage-table/branch-usage-table.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface BranchUsageColumn {\n  /** Keys into each row's `metrics`, e.g. \"compute_unit_seconds\". */\n  id: string;\n  /** Column header, e.g. \"Compute\". */\n  label: string;\n  /** Unit suffix beside the header, e.g. \"CU-hrs\". */\n  unit?: string;\n  /** Per-column formatter; falls back to two decimals. */\n  format?: (value: number) => string;\n}\n\nexport interface BranchUsageRow {\n  /** Branch id, e.g. \"br-young-sky-a1b2c3d4\". */\n  id: string;\n  /** Branch name, e.g. \"main\" or \"ci/pr-4821\". */\n  name: string;\n  /** Column id -> value, already converted to the display unit. */\n  metrics: Record<string, number>;\n  /** Marks the project's default branch. */\n  isDefault?: boolean;\n  /** Marks a protected branch. */\n  isProtected?: boolean;\n  /** Quiet second line, e.g. the parent branch or last-active time. */\n  hint?: string;\n}\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface BranchUsageSort {\n  /** Column id to sort by. */\n  columnId: string;\n  direction: SortDirection;\n}\n\nexport type BranchUsageTableProps = Omit<\n  ComponentProps<\"section\">,\n  \"children\" | \"onSelect\"\n> & {\n  /** One row per branch. */\n  rows: BranchUsageRow[];\n  /** Metric columns in display order; the first is the default sort. */\n  columns: BranchUsageColumn[];\n  /**\n   * \"table\" compares branches across metrics; \"list\" reads as a ranking\n   * of one. \"auto\" picks list for a single column, because one metric per\n   * row is a list and a table shape only adds a header and an axis.\n   */\n  layout?: \"auto\" | \"table\" | \"list\";\n  /** Panel heading. */\n  title?: string;\n  /** Controlled sort. */\n  sort?: BranchUsageSort;\n  /** Uncontrolled initial sort; defaults to the first column, descending. */\n  defaultSort?: BranchUsageSort;\n  onSortChange?: (sort: BranchUsageSort) => void;\n  /**\n   * Show only the top N rows and collapse the rest into one row that\n   * expands. Branch fleets run to hundreds; a table that long answers\n   * nothing.\n   */\n  topN?: number;\n  /** Adds a muted totals row across every branch, collapsed ones included. */\n  showTotals?: boolean;\n  /** Makes rows clickable, e.g. to open the branch. */\n  onSelectBranch?: (row: BranchUsageRow) => void;\n  /** Right-side header slot, e.g. a project filter. */\n  action?: ReactNode;\n  isLoading?: boolean;\n  error?: string | null;\n  /** Shown when `rows` is empty. */\n  empty?: ReactNode;\n  /** Quiet footer, e.g. a metering-lag notice. */\n  meteredThrough?: string;\n};\n\n/* ─────────────────────────────────────────────────────────\n * TABLE STORYBOARD\n *\n *  shape     a table earns its keep by comparing branches\n *            across metrics. With one metric it degrades to\n *            a list, because a lone column needs neither a\n *            header row nor an alignment axis\n *  rank      the sorted column carries a hairline rule\n *            along the bottom of its cell, left-anchored\n *            and scaled to the largest row. A filled block\n *            behind the digits reads as \"this cell is\n *            selected\"; a rule reads as magnitude\n *  sort      every sortable header shows its caret, muted\n *            until it's the active one. An affordance you\n *            only see after using it isn't an affordance\n *  tail      past topN the rest collapse into one row that\n *            says how many and opens on click. It sits\n *            below a rule, outside the ranking, so its\n *            value can't look like a broken sort\n *  total     a muted totals row across every branch, so one\n *            row's share is readable without doing the\n *            addition\n *  narrow    under 40rem the columns stack into label and\n *            value pairs — one DOM, no horizontal scroll\n * ───────────────────────────────────────────────────────── */\n\nconst DEFAULT_FORMAT = new Intl.NumberFormat(\"en-US\", {\n  maximumFractionDigits: 2,\n});\n\nconst TOTALS_ID = \"__totals__\";\nconst FULL_BAR = 100;\n/** Below this width the columns stack; matches the max-[40rem] variants. */\nconst STACK_AT = \"max-[40rem]\";\n\nconst formatCell = (column: BranchUsageColumn, value: number) =>\n  column.format ? column.format(value) : DEFAULT_FORMAT.format(value);\n\n/** Sorted columns announce their direction; the rest announce \"none\". */\nconst ariaSortOf = (isActive: boolean, direction: SortDirection) => {\n  if (!isActive) {\n    return \"none\" as const;\n  }\n\n  return direction === \"desc\"\n    ? (\"descending\" as const)\n    : (\"ascending\" as const);\n};\n\nconst sumBy = (rows: BranchUsageRow[], columnId: string) => {\n  let total = 0;\n\n  for (const row of rows) {\n    total += row.metrics[columnId] ?? 0;\n  }\n\n  return total;\n};\n\n/** Sorts, partitions past topN, and measures the ranked column. */\nconst shapeRows = (\n  rows: BranchUsageRow[],\n  activeSort: BranchUsageSort,\n  topN: number | undefined,\n  isTailOpen: boolean\n) => {\n  const ranked = [...rows].toSorted((a, b) => {\n    const left = a.metrics[activeSort.columnId] ?? 0;\n    const right = b.metrics[activeSort.columnId] ?? 0;\n\n    return activeSort.direction === \"desc\" ? right - left : left - right;\n  });\n\n  const isCollapsed = topN !== undefined && ranked.length > topN && !isTailOpen;\n  const head = isCollapsed ? ranked.slice(0, topN) : ranked;\n\n  return {\n    head,\n    peak: Math.max(\n      ...head.map((row) => row.metrics[activeSort.columnId] ?? 0),\n      0\n    ),\n    ranked,\n    tail: isCollapsed ? ranked.slice(topN) : [],\n  };\n};\n\nconst SortCaret = ({\n  direction,\n  isActive,\n}: {\n  direction: SortDirection;\n  isActive: boolean;\n}) => (\n  <span\n    aria-hidden=\"true\"\n    className={cn(\n      \"text-[10px]\",\n      isActive ? \"text-primary\" : \"text-muted-foreground/40\"\n    )}\n  >\n    {isActive && direction === \"asc\" ? \"▲\" : \"▼\"}\n  </span>\n);\n\nconst BranchTag = ({ children }: { children: ReactNode }) => (\n  <span className=\"rounded-full border border-border/60 px-1.5 py-px text-[10px] text-muted-foreground\">\n    {children}\n  </span>\n);\n\n/** Branch name, tags, and hint. Shared by both layouts. */\nconst BranchLabel = ({\n  isInteractive,\n  onSelect,\n  row,\n}: {\n  isInteractive: boolean;\n  onSelect?: () => void;\n  row: BranchUsageRow;\n}) => (\n  <>\n    <span className=\"flex items-center gap-1.5\">\n      {isInteractive ? (\n        <button\n          className=\"truncate rounded font-mono text-[13px] text-foreground transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\"\n          onClick={onSelect}\n          title={row.name}\n          type=\"button\"\n        >\n          {row.name}\n        </button>\n      ) : (\n        <span\n          className=\"truncate font-mono text-[13px] text-foreground\"\n          title={row.name}\n        >\n          {row.name}\n        </span>\n      )}\n      {row.isDefault ? <BranchTag>default</BranchTag> : null}\n      {row.isProtected ? <BranchTag>protected</BranchTag> : null}\n    </span>\n    {row.hint ? (\n      <span className=\"block truncate text-[10px] text-muted-foreground/70\">\n        {row.hint}\n      </span>\n    ) : null}\n  </>\n);\n\n/**\n * A metric cell. The share rule sits along the bottom edge rather than\n * behind the digits, so it can't be mistaken for a selection highlight.\n */\nconst MetricCell = ({\n  column,\n  isMuted,\n  isSorted,\n  share,\n  value,\n}: {\n  column: BranchUsageColumn;\n  isMuted: boolean;\n  isSorted: boolean;\n  share: number;\n  value: number;\n}) => (\n  <td\n    className={cn(\n      \"relative py-1.5 pl-3 text-right font-mono text-[13px] tabular-nums\",\n      `${STACK_AT}:flex ${STACK_AT}:justify-between ${STACK_AT}:pl-0`,\n      `${STACK_AT}:before:text-muted-foreground ${STACK_AT}:before:text-xs ${STACK_AT}:before:font-sans ${STACK_AT}:before:content-[attr(data-label)]`,\n      isMuted && \"text-muted-foreground\"\n    )}\n    data-label={column.label}\n  >\n    {formatCell(column, value)}\n    {isSorted ? (\n      <span\n        aria-hidden=\"true\"\n        className={`absolute bottom-1 left-3 h-px bg-primary/60 ${STACK_AT}:hidden`}\n        style={{ width: `calc((100% - 0.75rem) * ${share})` }}\n      />\n    ) : null}\n  </td>\n);\n\nconst SortableHeader = ({\n  activeSort,\n  column,\n  onToggle,\n}: {\n  activeSort: BranchUsageSort;\n  column: BranchUsageColumn;\n  onToggle: () => void;\n}) => {\n  const isActive = activeSort.columnId === column.id;\n\n  return (\n    <th\n      aria-sort={ariaSortOf(isActive, activeSort.direction)}\n      className=\"py-1.5 pl-3 text-right font-normal\"\n      scope=\"col\"\n    >\n      <button\n        className=\"inline-flex items-center gap-1 rounded text-muted-foreground text-xs transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\"\n        onClick={onToggle}\n        type=\"button\"\n      >\n        {column.label}\n        {column.unit ? (\n          <span className=\"text-[10px] text-muted-foreground/60\">\n            {column.unit}\n          </span>\n        ) : null}\n        <SortCaret direction={activeSort.direction} isActive={isActive} />\n      </button>\n    </th>\n  );\n};\n\n/** One metric: a ranked list, not a one-column table. */\nconst RankedList = ({\n  column,\n  isInteractive,\n  onOpenTail,\n  onSelectBranch,\n  peak,\n  rows,\n  showTotals,\n  tail,\n  totalRows,\n}: {\n  column: BranchUsageColumn;\n  isInteractive: boolean;\n  onOpenTail: () => void;\n  onSelectBranch?: (row: BranchUsageRow) => void;\n  peak: number;\n  rows: BranchUsageRow[];\n  showTotals: boolean;\n  tail: BranchUsageRow[];\n  totalRows: BranchUsageRow[];\n}) => (\n  <ul className=\"space-y-0.5\" data-slot=\"branch-usage-list\">\n    {rows.map((row) => {\n      const value = row.metrics[column.id] ?? 0;\n\n      return (\n        <li\n          className=\"border-border/30 border-b py-1.5 last:border-b-0\"\n          key={row.id}\n        >\n          <div className=\"flex items-baseline gap-3\">\n            <span className=\"min-w-0 flex-1\">\n              <BranchLabel\n                isInteractive={isInteractive}\n                onSelect={() => onSelectBranch?.(row)}\n                row={row}\n              />\n            </span>\n            <span className=\"font-mono text-[13px] text-foreground tabular-nums\">\n              {formatCell(column, value)}\n            </span>\n          </div>\n          <div aria-hidden=\"true\" className=\"mt-1 h-px w-full bg-muted\">\n            <div\n              className=\"h-px bg-primary/60\"\n              style={{ width: `${(peak > 0 ? value / peak : 0) * FULL_BAR}%` }}\n            />\n          </div>\n        </li>\n      );\n    })}\n\n    {/* The tail and the total belong in both layouts; a list that drops\n        them answers a smaller question than the table does. */}\n    {tail.length > 0 ? (\n      <li className=\"flex items-baseline gap-3 border-border/50 border-t pt-2\">\n        <button\n          className=\"flex-1 rounded text-left text-muted-foreground text-xs transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\"\n          onClick={onOpenTail}\n          type=\"button\"\n        >\n          {`${tail.length} other branches`}\n          <span className=\"ml-1.5 text-[10px] text-muted-foreground/60\">\n            show\n          </span>\n        </button>\n        <span className=\"font-mono text-[13px] text-muted-foreground tabular-nums\">\n          {formatCell(column, sumBy(tail, column.id))}\n        </span>\n      </li>\n    ) : null}\n\n    {showTotals ? (\n      <li className=\"flex items-baseline gap-3 border-border/50 border-t pt-2\">\n        <span className=\"flex-1 text-muted-foreground text-xs\">\n          {`all ${totalRows.length} branches`}\n        </span>\n        <span className=\"font-mono text-[13px] text-muted-foreground tabular-nums\">\n          {formatCell(column, sumBy(totalRows, column.id))}\n        </span>\n      </li>\n    ) : null}\n  </ul>\n);\n\nconst TailRow = ({\n  columns,\n  onOpen,\n  tail,\n}: {\n  columns: BranchUsageColumn[];\n  onOpen: () => void;\n  tail: BranchUsageRow[];\n}) => (\n  <tr\n    className={`border-border/50 border-t ${STACK_AT}:block ${STACK_AT}:py-2`}\n  >\n    <td className={`py-1.5 pr-3 ${STACK_AT}:block`}>\n      {/* Outside the ranking, so its value can't read as a sort gone wrong. */}\n      <button\n        className=\"rounded text-muted-foreground text-xs transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\"\n        onClick={onOpen}\n        type=\"button\"\n      >\n        {tail.length} other branches\n        <span className=\"ml-1.5 text-[10px] text-muted-foreground/60\">\n          show\n        </span>\n      </button>\n    </td>\n    {columns.map((column) => (\n      <MetricCell\n        column={column}\n        isMuted\n        isSorted={false}\n        key={column.id}\n        share={0}\n        value={sumBy(tail, column.id)}\n      />\n    ))}\n  </tr>\n);\n\nconst TotalsRow = ({\n  columns,\n  rows,\n}: {\n  columns: BranchUsageColumn[];\n  rows: BranchUsageRow[];\n}) => (\n  <tr\n    className={`border-border/50 border-t ${STACK_AT}:block ${STACK_AT}:py-2`}\n  >\n    <td\n      className={`py-1.5 pr-3 text-muted-foreground text-xs ${STACK_AT}:block`}\n    >\n      {`all ${rows.length} branches`}\n    </td>\n    {columns.map((column) => (\n      <MetricCell\n        column={column}\n        isMuted\n        isSorted={false}\n        key={`${TOTALS_ID}-${column.id}`}\n        share={0}\n        value={sumBy(rows, column.id)}\n      />\n    ))}\n  </tr>\n);\n\n/** The comparison layout: one row per branch, one column per metric. */\nconst UsageTable = ({\n  activeSort,\n  columns,\n  head,\n  isInteractive,\n  onOpenTail,\n  onSelectBranch,\n  onToggleSort,\n  peak,\n  showTotals,\n  tail,\n  totalRows,\n}: {\n  activeSort: BranchUsageSort;\n  columns: BranchUsageColumn[];\n  head: BranchUsageRow[];\n  isInteractive: boolean;\n  onOpenTail: () => void;\n  onSelectBranch?: (row: BranchUsageRow) => void;\n  onToggleSort: (columnId: string) => void;\n  peak: number;\n  showTotals: boolean;\n  tail: BranchUsageRow[];\n  totalRows: BranchUsageRow[];\n}) => (\n  <table className=\"w-full border-collapse text-xs\">\n    <thead className={`${STACK_AT}:hidden`}>\n      <tr className=\"border-border/50 border-b\">\n        <th\n          className=\"py-1.5 pr-3 text-left font-normal text-muted-foreground text-xs\"\n          scope=\"col\"\n        >\n          Branch\n        </th>\n        {columns.map((column) => (\n          <SortableHeader\n            activeSort={activeSort}\n            column={column}\n            key={column.id}\n            onToggle={() => onToggleSort(column.id)}\n          />\n        ))}\n      </tr>\n    </thead>\n    <tbody className={`${STACK_AT}:block`}>\n      {head.map((row) => (\n        <tr\n          className={cn(\n            \"border-border/30 border-b transition-colors last:border-b-0\",\n            isInteractive && \"hover:bg-muted/40\",\n            `${STACK_AT}:block ${STACK_AT}:py-2`\n          )}\n          key={row.id}\n        >\n          <td\n            className={`max-w-[220px] py-1.5 pr-3 ${STACK_AT}:block ${STACK_AT}:max-w-none`}\n          >\n            <BranchLabel\n              isInteractive={isInteractive}\n              onSelect={() => onSelectBranch?.(row)}\n              row={row}\n            />\n          </td>\n          {columns.map((column) => (\n            <MetricCell\n              column={column}\n              isMuted={false}\n              isSorted={column.id === activeSort.columnId}\n              key={column.id}\n              share={peak > 0 ? (row.metrics[column.id] ?? 0) / peak : 0}\n              value={row.metrics[column.id] ?? 0}\n            />\n          ))}\n        </tr>\n      ))}\n    </tbody>\n\n    {tail.length > 0 || showTotals ? (\n      <tfoot className={`${STACK_AT}:block`}>\n        {tail.length > 0 ? (\n          <TailRow columns={columns} onOpen={onOpenTail} tail={tail} />\n        ) : null}\n        {showTotals ? <TotalsRow columns={columns} rows={totalRows} /> : null}\n      </tfoot>\n    ) : null}\n  </table>\n);\n\n/** Empty, list, or table: one decision, made once. */\nconst UsageBody = ({\n  activeSort,\n  asList,\n  columns,\n  empty,\n  head,\n  isInteractive,\n  onOpenTail,\n  onSelectBranch,\n  onToggleSort,\n  peak,\n  ranked,\n  showTotals,\n  tail,\n  totalRows,\n}: {\n  activeSort: BranchUsageSort;\n  asList: boolean;\n  columns: BranchUsageColumn[];\n  empty?: ReactNode;\n  head: BranchUsageRow[];\n  isInteractive: boolean;\n  onOpenTail: () => void;\n  onSelectBranch?: (row: BranchUsageRow) => void;\n  onToggleSort: (columnId: string) => void;\n  peak: number;\n  ranked: BranchUsageRow[];\n  showTotals: boolean;\n  tail: BranchUsageRow[];\n  totalRows: BranchUsageRow[];\n}) => {\n  const [firstColumn] = columns;\n\n  if (ranked.length === 0) {\n    return (\n      <div className=\"flex h-24 items-center justify-center rounded-md border border-border/50 border-dashed\">\n        {empty ?? (\n          <p className=\"text-muted-foreground text-xs\">\n            no branch consumption in this window\n          </p>\n        )}\n      </div>\n    );\n  }\n\n  if (asList && firstColumn) {\n    return (\n      <RankedList\n        column={firstColumn}\n        isInteractive={isInteractive}\n        onOpenTail={onOpenTail}\n        onSelectBranch={onSelectBranch}\n        peak={peak}\n        rows={head}\n        showTotals={showTotals}\n        tail={tail}\n        totalRows={totalRows}\n      />\n    );\n  }\n\n  return (\n    <UsageTable\n      activeSort={activeSort}\n      columns={columns}\n      head={head}\n      isInteractive={isInteractive}\n      onOpenTail={onOpenTail}\n      onSelectBranch={onSelectBranch}\n      onToggleSort={onToggleSort}\n      peak={peak}\n      showTotals={showTotals}\n      tail={tail}\n      totalRows={totalRows}\n    />\n  );\n};\n\nconst LoadingCard = ({ className, ...props }: ComponentProps<\"section\">) => (\n  <section\n    className={cn(\"rounded-lg border border-border/60 bg-card p-4\", className)}\n    data-slot=\"branch-usage-table\"\n    {...props}\n  >\n    <Skeleton className=\"h-4 w-32\" />\n    <div className=\"mt-4 space-y-2.5\">\n      {Array.from({ length: 5 }, (_, index) => index).map((index) => (\n        <Skeleton className=\"h-7 w-full\" key={index} />\n      ))}\n    </div>\n  </section>\n);\n\nexport const BranchUsageTable = ({\n  action,\n  className,\n  columns,\n  defaultSort,\n  empty,\n  error = null,\n  isLoading = false,\n  layout = \"auto\",\n  meteredThrough,\n  onSelectBranch,\n  onSortChange,\n  rows,\n  showTotals = true,\n  sort,\n  title = \"usage by branch\",\n  topN,\n  ...props\n}: BranchUsageTableProps) => {\n  const [firstColumn] = columns;\n  const [internalSort, setInternalSort] = useState<BranchUsageSort>(\n    defaultSort ?? { columnId: firstColumn?.id ?? \"\", direction: \"desc\" }\n  );\n  const [isTailOpen, setIsTailOpen] = useState(false);\n  const activeSort = sort ?? internalSort;\n\n  const toggleSort = (columnId: string) => {\n    const isActive = activeSort.columnId === columnId;\n    const next: BranchUsageSort = {\n      columnId,\n      direction: isActive && activeSort.direction === \"desc\" ? \"asc\" : \"desc\",\n    };\n\n    if (sort === undefined) {\n      setInternalSort(next);\n    }\n\n    onSortChange?.(next);\n  };\n\n  const { head, peak, ranked, tail } = shapeRows(\n    rows,\n    activeSort,\n    topN,\n    isTailOpen\n  );\n  const isInteractive = Boolean(onSelectBranch);\n  const asList =\n    layout === \"list\" || (layout === \"auto\" && columns.length <= 1);\n\n  if (isLoading) {\n    return <LoadingCard className={className} {...props} />;\n  }\n\n  return (\n    <section\n      className={cn(\n        \"rounded-lg border border-border/60 bg-card p-4\",\n        className\n      )}\n      data-slot=\"branch-usage-table\"\n      {...props}\n    >\n      <header className=\"mb-3 flex items-center justify-between gap-3\">\n        <h3 className=\"font-mono text-muted-foreground text-xs\">{title}</h3>\n        {action}\n      </header>\n\n      {error ? (\n        <p\n          className=\"mb-3 flex items-baseline gap-2 text-sm\"\n          data-slot=\"branch-usage-table-error\"\n          role=\"alert\"\n        >\n          <span className=\"font-mono text-destructive text-xs\">error</span>\n          <span className=\"text-foreground\">{error}</span>\n        </p>\n      ) : null}\n\n      <UsageBody\n        activeSort={activeSort}\n        asList={asList}\n        columns={columns}\n        empty={empty}\n        head={head}\n        isInteractive={isInteractive}\n        onOpenTail={() => setIsTailOpen(true)}\n        onSelectBranch={onSelectBranch}\n        onToggleSort={toggleSort}\n        peak={peak}\n        ranked={ranked}\n        showTotals={showTotals}\n        tail={tail}\n        totalRows={rows}\n      />\n\n      {meteredThrough ? (\n        <p\n          className=\"mt-3 text-[10px] text-muted-foreground/70 tabular-nums\"\n          data-slot=\"branch-usage-table-lag\"\n        >\n          {meteredThrough}\n        </p>\n      ) : null}\n    </section>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}