{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "segmented-bar",
  "title": "Segmented Bar",
  "description": "Stacked bar showing the parts of a total, with an optional legend.",
  "dependencies": [
    "class-variance-authority@^0.7.1"
  ],
  "registryDependencies": [
    "@pestoui/style",
    "@pestoui/utils"
  ],
  "files": [
    {
      "path": "pestoui/components/segmented-bar.tsx",
      "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* A stacked breakdown — \"18 runs: 9 queued, 6 running, 3 done\" — as\n   one bar. Deliberately not a mode of `Progress`: that component reports a\n   single value against a max and hands Radix the `progressbar` role and the\n   `aria-value*` trio, none of which can express three categories at once. The\n   shapes rhyme; the ARIA contracts do not, so they stay separate components.\n\n   One tone map feeds both the band and its legend dot. The dot is the only\n   thing tying a legend row back to the band it names, so a swatch that\n   resolves to a different token has stopped being a key — sourcing both cva\n   calls from one record makes that a compile-time fact instead of a habit. */\nconst TONE_FILL = {\n  \"chart-1\": \"bg-chart-1\",\n  \"chart-2\": \"bg-chart-2\",\n  \"chart-3\": \"bg-chart-3\",\n  \"chart-4\": \"bg-chart-4\",\n  \"chart-5\": \"bg-chart-5\",\n  selected: \"bg-selected\",\n  success: \"bg-success\",\n  warning: \"bg-warning\",\n  destructive: \"bg-destructive\",\n  info: \"bg-info\",\n  muted: \"bg-muted-foreground\",\n} as const\n\n/* The rounded ends come from clipping the track, not from rounding the first\n   and last bands. Which band is first changes with the data — a zero-value\n   band is dropped rather than drawn, and the muted tail only exists while the\n   total runs ahead of the sum — so a `first:`/`last:` radius keeps re-attaching\n   itself to whatever survived the filter. A clip cannot get that wrong.\n\n   The track's own fill is the remainder: everything the bands do not cover\n   reads as the empty tail, which is why there is no element for it. */\nconst segmentedBarTrackVariants = cva(\n  \"flex w-full overflow-hidden rounded-full bg-muted\",\n  {\n    variants: {\n      size: {\n        sm: \"h-1.5\",\n        default: \"h-2\",\n        lg: \"h-3\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  }\n)\n\n/* The gap between two bands is a transparent left border with the fill clipped\n   to the padding box, so what shows through it is the track itself. Margin was\n   the obvious alternative and it is the wrong one: it has to come out of every\n   width, at which point the widths stop being the percentages they claim to\n   be. This leaves each one exact and costs no colour token — the gap is\n   whatever the track is painted, in light mode and dark.\n\n   `shrink-0` because flex would otherwise silently rescale the whole row the\n   moment sub-pixel rounding pushed the sum a hair past 100%, turning every\n   band into an approximation of the number it is reporting.\n\n   Widths glide on the same overdamped curve `Progress` uses, and for the same\n   reason: a band that overshoots has, for a few frames, drawn a share that is\n   not true. Inline widths land on the first paint, so this only runs when the\n   data actually changes. */\nconst segmentedBarSegmentVariants = cva(\n  \"h-full shrink-0 border-l-2 border-transparent bg-clip-padding transition-[width] duration-spring-glide ease-spring-glide first:border-l-0\",\n  {\n    variants: {\n      tone: TONE_FILL,\n    },\n    defaultVariants: {\n      tone: \"selected\",\n    },\n  }\n)\n\nconst segmentedBarDotVariants = cva(\"size-2 shrink-0 rounded-full\", {\n  variants: {\n    tone: TONE_FILL,\n  },\n  defaultVariants: {\n    tone: \"selected\",\n  },\n})\n\ntype SegmentedBarTone = NonNullable<\n  VariantProps<typeof segmentedBarSegmentVariants>[\"tone\"]\n>\n\ntype BarSegment = {\n  id: string\n  label: string\n  /** An absolute count, not a percentage. Negative and non-finite read as 0. */\n  value: number\n  tone?: SegmentedBarTone\n}\n\ntype SegmentedBarProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n  VariantProps<typeof segmentedBarTrackVariants> & {\n    segments: BarSegment[]\n    /** Denominator. Above the sum it leaves an empty tail; defaults to the sum. */\n    total?: number\n    /** Caption above the bar — e.g. \"18 runs\". */\n    label?: React.ReactNode\n    showLegend?: boolean\n  }\n\nfunction SegmentedBar({\n  segments,\n  total,\n  label,\n  showLegend = false,\n  size = \"default\",\n  className,\n  ...props\n}: SegmentedBarProps) {\n  const bands = segments.map((segment) => ({\n    ...segment,\n    value: Number.isFinite(segment.value) ? Math.max(0, segment.value) : 0,\n  }))\n  const sum = bands.reduce((running, band) => running + band.value, 0)\n\n  /* Two failure modes, one denominator. A total under the sum would run the\n     bands off the end of the track; a total of 0 against no data would divide\n     by zero. Widening the denominator to the sum fixes the first without\n     lying — the bar simply fills completely and each band still shows its true\n     share of what was passed — and the explicit `> 0` test below means an\n     empty bar paints an empty track rather than a row of NaN widths. */\n  const declared =\n    typeof total === \"number\" && Number.isFinite(total) ? Math.max(0, total) : sum\n  const denominator = Math.max(declared, sum)\n  const remainder = denominator - sum\n  const share = (value: number) => (denominator > 0 ? (value / denominator) * 100 : 0)\n\n  /* A stack of coloured rectangles carries nothing to a screen reader, so the\n     whole breakdown is spelled out here. Zero-value bands are named even\n     though they are not drawn — the legend lists them, and the two should\n     agree about which categories exist. */\n  const summary = [\n    `Total ${denominator}`,\n    ...bands.map((band) => `${band.label}: ${band.value}`),\n    ...(remainder > 0 ? [`Remaining: ${remainder}`] : []),\n  ].join(\", \")\n\n  return (\n    <div\n      data-slot=\"segmented-bar\"\n      data-size={size}\n      className={cn(\"flex w-full min-w-0 flex-col gap-3\", className)}\n      {...props}\n    >\n      {label ? (\n        <div\n          data-slot=\"segmented-bar-label\"\n          className=\"text-sm leading-none font-medium text-foreground\"\n        >\n          {label}\n        </div>\n      ) : null}\n\n      <div\n        data-slot=\"segmented-bar-track\"\n        role=\"img\"\n        aria-label={summary}\n        className={cn(segmentedBarTrackVariants({ size }))}\n      >\n        {bands\n          .filter((band) => band.value > 0)\n          .map((band) => (\n            <div\n              key={band.id}\n              data-slot=\"segmented-bar-segment\"\n              data-tone={band.tone ?? \"selected\"}\n              className={cn(segmentedBarSegmentVariants({ tone: band.tone }))}\n              style={{ width: `${share(band.value)}%` }}\n            />\n          ))}\n      </div>\n\n      {showLegend ? (\n        <ul data-slot=\"segmented-bar-legend\" className=\"flex min-w-0 flex-col gap-2\">\n          {bands.map((band) => (\n            <li\n              key={band.id}\n              data-slot=\"segmented-bar-legend-item\"\n              data-tone={band.tone ?? \"selected\"}\n              className=\"flex items-center gap-2 text-sm leading-none\"\n            >\n              <span\n                aria-hidden=\"true\"\n                className={cn(segmentedBarDotVariants({ tone: band.tone }))}\n              />\n              <span className=\"min-w-0 truncate text-muted-foreground\">\n                {band.label}\n              </span>\n              <span className=\"ml-auto font-medium tabular-nums text-foreground\">\n                {band.value}\n              </span>\n            </li>\n          ))}\n        </ul>\n      ) : null}\n    </div>\n  )\n}\n\nexport {\n  SegmentedBar,\n  segmentedBarDotVariants,\n  segmentedBarSegmentVariants,\n  segmentedBarTrackVariants,\n}\nexport type { BarSegment, SegmentedBarProps, SegmentedBarTone }\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}