{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bar-list","type":"registry:ui","title":"Bar List","description":"Values side by side as labelled bars: top products, traffic sources, storage by type.","dependencies":[],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-motion.json"],"files":[{"path":"registry/dashral/bar-list.tsx","type":"registry:file","target":"components/dashral/bar-list.tsx","content":"import * as React from \"react\"\n\nimport { enterMotion, growMotion, stagger } from \"./lib/motion\"\nimport { cn } from \"./lib/utils\"\n\ntype BarListItem = {\n  /** A stable key; the index is used without one. */\n  key?: string\n  label: React.ReactNode\n  value: number\n  /** Before the label, e.g. an avatar, a product photo or an icon. */\n  icon?: React.ReactNode\n  /** Muted text after the label, e.g. the referring domains. */\n  hint?: React.ReactNode\n  /** Muted text before the value, e.g. \"53 sold\". */\n  meta?: React.ReactNode\n  /** Shown in place of the formatted value, e.g. \"2m 05s\". */\n  display?: React.ReactNode\n  /** Makes the label a link. */\n  href?: string\n  /** Any CSS color for this bar, e.g. \"var(--chart-2)\"; otherwise the list's color. */\n  color?: string\n}\n\ntype BarListColor = \"primary\" | \"neutral\" | \"info\" | \"success\" | \"warning\" | \"destructive\" | \"highlight\"\n\ntype BarListProps = Omit<React.HTMLAttributes<HTMLElement>, \"children\" | \"color\"> & {\n  items: BarListItem[]\n  /** The bars' color. */\n  color?: BarListColor\n  /** Numbers the rows (an ordered list), for a ranking. */\n  showRank?: boolean\n  /** Each value's share of `total` as a percentage, after the value. */\n  showShare?: boolean\n  /** What shares are of; by default the values added up. Pass it when the list is a top N of more. */\n  total?: number\n  /** The value a full bar stands for; by default the largest. */\n  max?: number\n  /** BCP 47 locale for values and shares. */\n  locale?: string\n  /** How values are written, e.g. `{ style: \"currency\", currency: \"USD\" }`. */\n  formatOptions?: Intl.NumberFormatOptions\n  /** Your router's link for rows with href, e.g. Next.js Link. */\n  linkAs?: React.ElementType\n  classNames?: { item?: string; icon?: string; label?: string; value?: string; track?: string; bar?: string }\n}\n\nconst barColors: Record<BarListColor, string> = {\n  primary: \"bg-primary\",\n  neutral: \"bg-foreground/70\",\n  info: \"bg-info\",\n  success: \"bg-success\",\n  warning: \"bg-warning\",\n  destructive: \"bg-destructive\",\n  highlight: \"bg-highlight\",\n}\n\n/**\n * Values side by side as labelled bars, in one tag: top products, traffic sources, storage by type. Every value is\n * written out, so the bars only add a quick sense of size; they grow in one after another.\n */\nfunction BarList({\n  items,\n  color = \"primary\",\n  showRank = false,\n  showShare = false,\n  total,\n  max,\n  locale = \"en-US\",\n  formatOptions,\n  linkAs: LinkAs = \"a\",\n  className,\n  classNames,\n  ...props\n}: BarListProps) {\n  const format = new Intl.NumberFormat(locale, formatOptions)\n  const percent = new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 0 })\n  const full = max ?? Math.max(0, ...items.map((item) => item.value))\n  const sum = total ?? items.reduce((acc, item) => acc + Math.max(0, item.value), 0)\n  const List = showRank ? \"ol\" : \"ul\"\n\n  return (\n    <List data-slot=\"bar-list\" className={cn(\"grid grid-cols-1 gap-3.5 text-sm\", className)} {...props}>\n      {items.map((item, i) => (\n        <li key={item.key ?? i} className={cn(enterMotion, \"flex items-start gap-2.5\", classNames?.item)} style={stagger(i, 60)}>\n          {showRank && (\n            <span aria-hidden className=\"mt-0.5 w-4 shrink-0 text-xs leading-5 text-muted-foreground tabular-nums\">\n              {i + 1}\n            </span>\n          )}\n          {item.icon && <span className={cn(\"flex shrink-0 items-center text-muted-foreground [&_svg]:size-4\", classNames?.icon)}>{item.icon}</span>}\n          <div className=\"grid min-w-0 flex-1 grid-cols-1 gap-1.5\">\n            <div className=\"flex min-h-5 items-center gap-2.5\">\n              <span className={cn(\"flex min-w-0 flex-1 items-baseline gap-2\", classNames?.label)}>\n                {item.href ? (\n                  <LinkAs href={item.href} className=\"max-w-full shrink-0 truncate font-medium underline-offset-4 hover:underline\">\n                    {item.label}\n                  </LinkAs>\n                ) : (\n                  <span className=\"truncate font-medium\">{item.label}</span>\n                )}\n                {item.hint != null && <span className=\"min-w-0 truncate text-xs text-muted-foreground\">{item.hint}</span>}\n              </span>\n              {item.meta != null && <span className=\"shrink-0 text-xs text-muted-foreground tabular-nums\">{item.meta}</span>}\n              <span className={cn(\"shrink-0 text-end font-semibold tabular-nums\", classNames?.value)}>{item.display ?? format.format(item.value)}</span>\n              {showShare && (\n                <span className=\"w-9 shrink-0 text-end text-xs text-muted-foreground tabular-nums\">\n                  {percent.format(sum ? Math.max(0, item.value) / sum : 0)}\n                </span>\n              )}\n            </div>\n            <div aria-hidden className={cn(\"h-1.5 overflow-hidden rounded-full bg-muted\", classNames?.track)}>\n              <div\n                className={cn(growMotion, \"h-full rounded-full\", !item.color && barColors[color], classNames?.bar)}\n                style={{ width: `${full ? (Math.max(0, item.value) / full) * 100 : 0}%`, background: item.color, ...stagger(i, 60) }}\n              />\n            </div>\n          </div>\n        </li>\n      ))}\n    </List>\n  )\n}\n\nexport { BarList, type BarListProps, type BarListItem, type BarListColor }\n"}],"cssVars":{"theme":{"color-info":"var(--info)","color-info-foreground":"var(--info-foreground)","color-info-text":"var(--info-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","color-warning":"var(--warning)","color-warning-foreground":"var(--warning-foreground)","color-warning-text":"var(--warning-text)","color-highlight":"var(--highlight)","color-highlight-foreground":"var(--highlight-foreground)","color-highlight-text":"var(--highlight-text)"},"light":{"info":"oklch(0.5 0.134 242.749)","info-foreground":"oklch(0.985 0 0)","info-text":"oklch(0.443 0.11 240.79)","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","warning":"#f59200","warning-foreground":"oklch(0.3 0.07 60)","warning-text":"#964e00","highlight":"#b8008c","highlight-foreground":"oklch(0.985 0 0)","highlight-text":"#a3007b"},"dark":{"info":"oklch(0.685 0.169 237.323)","info-foreground":"oklch(0.2 0 0)","info-text":"oklch(0.746 0.16 232.661)","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)","warning":"#f59200","warning-foreground":"oklch(0.25 0.06 60)","warning-text":"#ffba42","highlight":"#ff2ecf","highlight-foreground":"oklch(0.2 0 0)","highlight-text":"#ff75df"}}}