{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"split-bar","type":"registry:ui","title":"Split bar","description":"One bar that is the whole, split into shares in the chart colours. Rest on a share, or focus its legend row, and it widens to show its figures while the others make room; switch shares off and the rest fill the bar again.","dependencies":["clsx","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/split-bar.tsx","type":"registry:ui","target":"components/ui/split-bar.tsx","content":"\"use client\";\nimport { useState, type ComponentProps, type CSSProperties } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./split-bar.css\";\n\nexport type SplitBarSegment = { id: string; label: string; value: number };\n\nexport type SplitBarProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  /** Names the whole, e.g. \"Revenue by plan\". */\n  label: string;\n  segments: SplitBarSegment[];\n  /** Formats a segment's own value for the readout. */\n  format?: (value: number) => string;\n};\n\nconst percent = (value: number, total: number) => (total ? Math.round((value / total) * 1000) / 10 : 0);\n\n/**\n * One bar that is the whole, split into shares in the chart colours. Rest on a share, or focus its legend row, and it\n * widens to show its exact figures while the rest make room; switch shares off in the legend and the others spring\n * to fill the bar, with their percentages of what is left. Under reduced motion the widths change without the glide.\n */\nexport function SplitBar({ label, segments, format = (value) => value.toLocaleString(\"en-US\"), className, ...props }: SplitBarProps) {\n  const [hidden, setHidden] = useState<ReadonlySet<string>>(new Set());\n  const [active, setActive] = useState<string | null>(null);\n  const shown = segments.filter((segment) => !hidden.has(segment.id));\n  const total = shown.reduce((sum, segment) => sum + segment.value, 0);\n  const focus = shown.find((segment) => segment.id === active);\n\n  const toggle = (id: string) =>\n    setHidden((current) => {\n      const next = new Set(current);\n      if (next.has(id)) next.delete(id);\n      else if (shown.length > 1) next.add(id); // the bar always keeps one share\n      return next;\n    });\n\n  return (\n    <div data-slot=\"split-bar\" className={cn(\"grid gap-3\", className)} {...props}>\n      <div data-slot=\"sb-readout\" aria-live=\"polite\" className=\"flex min-h-6 items-baseline justify-between gap-3 text-sm\">\n        {focus ? (\n          <>\n            <span className=\"truncate font-medium\">{focus.label}</span>\n            <span className=\"shrink-0 tabular-nums\">\n              {format(focus.value)} <span className=\"text-muted-foreground\">· {percent(focus.value, total)}%</span>\n            </span>\n          </>\n        ) : (\n          <>\n            <span className=\"truncate text-muted-foreground\">{label}</span>\n            <span className=\"shrink-0 tabular-nums text-muted-foreground\">{format(total)}</span>\n          </>\n        )}\n      </div>\n      <div data-slot=\"sb-bar\" role=\"img\" aria-label={`${label}: ${shown.map((segment) => `${segment.label} ${percent(segment.value, total)}%`).join(\", \")}`} data-focused={focus ? \"\" : undefined}>\n        {segments.map((segment, index) => {\n          const off = hidden.has(segment.id);\n          // The focused share takes an eighth of the bar on top of its own size, so even a sliver opens up to read.\n          const grow = off ? 0 : segment.value + (segment.id === active ? total * 0.12 : 0);\n          return (\n            <span\n              key={segment.id}\n              data-slot=\"sb-segment\"\n              data-active={segment.id === active ? \"\" : undefined}\n              data-off={off ? \"\" : undefined}\n              style={{ flexGrow: grow, \"--sb-color\": `var(--chart-${(index % 6) + 1})` } as CSSProperties}\n              onPointerEnter={() => setActive(segment.id)}\n              onPointerLeave={() => setActive(null)}\n            >\n              <span data-slot=\"sb-share\">{percent(segment.value, total)}%</span>\n            </span>\n          );\n        })}\n      </div>\n      <ul data-slot=\"sb-legend\" aria-label={`${label} shares`} className=\"flex flex-wrap gap-x-1 gap-y-1\">\n        {segments.map((segment, index) => {\n          const off = hidden.has(segment.id);\n          return (\n            <li key={segment.id}>\n              <button\n                type=\"button\"\n                data-slot=\"sb-key\"\n                aria-pressed={!off}\n                aria-disabled={!off && shown.length === 1 ? true : undefined}\n                style={{ \"--sb-color\": `var(--chart-${(index % 6) + 1})` } as CSSProperties}\n                onClick={() => toggle(segment.id)}\n                onPointerEnter={() => !off && setActive(segment.id)}\n                onPointerLeave={() => setActive(null)}\n                onFocus={() => !off && setActive(segment.id)}\n                onBlur={() => setActive(null)}\n              >\n                <span data-slot=\"sb-swatch\" aria-hidden=\"true\" />\n                {segment.label}\n                <span className=\"tabular-nums text-muted-foreground\">{off ? \"Off\" : `${percent(segment.value, total)}%`}</span>\n              </button>\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n}\n"},{"path":"ui/split-bar.css","type":"registry:file","target":"components/ui/split-bar.css","content":"/* split-bar.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Split bar: one whole; the focused share takes extra room and the rest make way. */\n  [data-slot=\"split-bar\"] [data-slot=\"sb-bar\"] { display: flex; height: 36px; overflow: hidden; border-radius: var(--radius-control); }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-segment\"] { position: relative; display: flex; align-items: center; flex-basis: 0; min-width: 0; padding-inline: 8px; overflow: hidden; border-left: 2px solid var(--background); background: var(--sb-color); transition: flex-grow var(--duration-travel) var(--ease-settle), opacity var(--duration-normal) var(--ease-out), border-left-width var(--duration-travel) var(--ease-out); }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-segment\"]:first-child, [data-slot=\"split-bar\"] [data-slot=\"sb-segment\"][data-off] { border-left-width: 0; }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-bar\"][data-focused] [data-slot=\"sb-segment\"]:not([data-active]) { opacity: 0.4; }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-share\"] { font-size: 12px; font-weight: 500; font-variant-numeric: tabular-nums; color: var(--background); white-space: nowrap; opacity: 0; translate: 0 4px; transition: opacity var(--duration-normal) var(--ease-out), translate var(--duration-normal) var(--ease-out); }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-segment\"][data-active] [data-slot=\"sb-share\"] { opacity: 1; translate: 0 0; transition-delay: 80ms; }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-key\"] { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 8px; border: 0; border-radius: var(--radius-control); background: none; color: var(--foreground); font-size: 13px; cursor: pointer; transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out); }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-key\"]:hover { background: var(--muted); }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-key\"][aria-pressed=\"false\"] { color: var(--muted-foreground); }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-key\"][aria-disabled=\"true\"] { cursor: default; }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-key\"]:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-swatch\"] { width: 8px; height: 8px; border-radius: 2px; background: var(--sb-color); transition: background-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out); }\n  [data-slot=\"split-bar\"] [data-slot=\"sb-key\"][aria-pressed=\"false\"] [data-slot=\"sb-swatch\"] { background: transparent; box-shadow: inset 0 0 0 1.5px var(--sb-color); }\n  @media (pointer: coarse) { [data-slot=\"split-bar\"] [data-slot=\"sb-key\"] { height: 36px; } }\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"split-bar\"] [data-slot=\"sb-segment\"], [data-slot=\"split-bar\"] [data-slot=\"sb-share\"] { transition: none; }\n  }\n}\n"}],"categories":["motion"],"meta":{"tier":"free","license":"MIT","family":"changing-values","docs":"/docs/components/split-bar"}}