{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bar-list",
  "title": "Bar List",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A ranked list of bars with the name and number on top of the bar, so both stay readable at any width. Rows can be links or buttons, a long list folds to its top few, and every value is real text.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/bar-list.tsx",
      "content": "// Ballmac UI: Bar List. https://ui.ballmac.com/components/bar-list\n\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown, ExternalLink } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype BarListItem = {\n  id: string\n  label: string\n  value: number\n  /** Makes the label a link. */\n  href?: string\n  /** A small icon or logo before the label. */\n  icon?: React.ReactNode\n  /** A short note after the value, for example \"+12%\". */\n  hint?: string\n}\n\ntype BarListProps = Omit<React.ComponentProps<\"div\">, \"onSelect\" | \"title\"> & {\n  items: BarListItem[]\n  /** Heading above the list. */\n  title?: string\n  /** Formats each value. Defaults to a grouped number. */\n  format?: (value: number) => string\n  /** Shows only this many items, with a button to show the rest. */\n  limit?: number\n  /** Sorts by value, largest first. Defaults to true. */\n  sort?: boolean\n  /** Shows each item's share of the total as a percent. */\n  showShare?: boolean\n  /** Makes rows buttons. Called with the item id. */\n  onSelect?: (id: string) => void\n  /** The id of the selected row, when `onSelect` is used. */\n  selected?: string | null\n}\n\nconst nf = new Intl.NumberFormat(\"en-US\")\n\n/**\n * A ranked list of bars: the bar sits behind the label so the name and the number stay readable at any width, rows can be links or buttons, and a long list folds\n * to its top few. The values are text, so nothing depends on the bars.\n */\nfunction BarList({ items, title, format = (v) => nf.format(v), limit, sort = true, showShare = false, onSelect, selected, className, ...props }: BarListProps) {\n  const msg = useMessages()\n  const id = React.useId()\n  const [open, setOpen] = React.useState(false)\n  const ordered = React.useMemo(() => (sort ? [...items].sort((a, b) => b.value - a.value) : items), [items, sort])\n  const max = Math.max(1, ...ordered.map((i) => i.value))\n  const total = ordered.reduce((s, i) => s + i.value, 0)\n  const shown = limit && !open ? ordered.slice(0, limit) : ordered\n  const hidden = ordered.length - shown.length\n\n  return (\n    <div data-slot=\"bar-list\" className={cn(\"grid min-w-0 gap-3\", className)} {...props}>\n      {title && <h3 id={`${id}-t`} className=\"text-sm font-medium\">{title}</h3>}\n      <ol aria-labelledby={title ? `${id}-t` : undefined} className=\"grid grid-cols-[minmax(0,1fr)] gap-1.5\">\n        {shown.map((it, i) => {\n          const pct = Math.max(2, (it.value / max) * 100)\n          const on = selected === it.id\n          const label = (\n            <span className=\"flex min-w-0 items-center gap-2\">\n              {it.icon && <span aria-hidden=\"true\" className=\"flex size-4 shrink-0 items-center justify-center [&>svg]:size-4\">{it.icon}</span>}\n              <span className=\"truncate\">{it.label}</span>\n              {it.href && <ExternalLink aria-hidden=\"true\" className=\"size-3 shrink-0 text-muted-foreground rtl:-scale-x-100\" />}\n            </span>\n          )\n          const row = \"relative flex min-h-9 min-w-0 items-center justify-between gap-3 overflow-hidden rounded-md px-3 text-sm outline-none\"\n          const bar = (\n            <span\n              aria-hidden=\"true\"\n              className={cn(\"absolute inset-y-0 start-0 rounded-md transition-[width] duration-500 ease-out motion-reduce:transition-none\", on ? \"bg-foreground/20\" : \"bg-muted\")}\n              style={{ width: `${pct}%` }}\n            />\n          )\n          const value = (\n            <span className=\"relative flex shrink-0 items-baseline gap-2 tabular-nums\">\n              {it.hint && <span className=\"text-xs text-muted-foreground\">{it.hint}</span>}\n              {showShare && total > 0 && <span className=\"text-xs text-muted-foreground\">{msg(\"bar-list.share\", \"{percent}%\", { percent: Math.round((it.value / total) * 100) })}</span>}\n              <span className=\"font-medium\">{format(it.value)}</span>\n            </span>\n          )\n          const content = (\n            <>\n              {bar}\n              <span className=\"relative min-w-0\">{label}</span>\n              {value}\n            </>\n          )\n          return (\n            <li key={it.id} className=\"min-w-0\">\n              {it.href ? (\n                <a href={it.href} className={cn(row, \"focus-visible:ring-[3px] focus-visible:ring-ring/50 hover:[&>span:first-child]:bg-foreground/15\")}>\n                  {content}\n                </a>\n              ) : onSelect ? (\n                <button type=\"button\" aria-pressed={on} onClick={() => onSelect(it.id)} className={cn(row, \"w-full text-start focus-visible:ring-[3px] focus-visible:ring-ring/50\")}>\n                  {content}\n                </button>\n              ) : (\n                <div className={row}>{content}</div>\n              )}\n              <span className=\"sr-only\">{msg(\"bar-list.rank\", \"Number {rank}\", { rank: i + 1 })}</span>\n            </li>\n          )\n        })}\n      </ol>\n      {limit && ordered.length > limit && (\n        <button type=\"button\" aria-expanded={open} onClick={() => setOpen((v) => !v)} className=\"inline-flex h-8 w-fit items-center gap-1.5 rounded-md px-2 text-xs font-medium text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\">\n          <ChevronDown aria-hidden=\"true\" className={cn(\"size-3.5 transition-transform motion-reduce:transition-none\", open && \"rotate-180\")} />\n          {open ? msg(\"bar-list.less\", \"Show fewer\") : msg(\"bar-list.more\", \"Show {count} more\", { count: hidden })}\n        </button>\n      )}\n    </div>\n  )\n}\n\nexport { BarList, type BarListItem, type BarListProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/bar-list.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "chart",
      "ranking",
      "top list",
      "analytics",
      "bars",
      "dashboard",
      "leaderboard"
    ],
    "version": "1.0.0",
    "updated": "2026-10-25",
    "tests": true,
    "ai": {
      "summary": "items is [{ id, label, value, href?, icon?, hint? }]. Sorted by value unless sort is false. title, format(value), showShare (percent of total), limit (folds the rest behind a Show N more button). href makes a row a link; onSelect(id) with selected makes rows toggle buttons. The bar is decoration behind real text.",
      "whenToUse": [
        "Top pages, sources, countries, products or errors in an analytics view",
        "A leaderboard where the label matters more than the shape"
      ],
      "whenNotToUse": [
        "Comparing a few values precisely over time; use chart-bar",
        "Part-to-whole with many parts; use treemap or split-bar"
      ],
      "composesWith": [
        "split-bar",
        "funnel-chart",
        "stat-card",
        "kpi-row"
      ],
      "a11y": [
        {
          "keys": "List",
          "action": "An ordered list; each row also says 'Number n' for screen readers"
        },
        {
          "keys": "Rows",
          "action": "Links and buttons are native; buttons have pressed state"
        },
        {
          "keys": "Fold",
          "action": "A button with aria-expanded"
        },
        {
          "keys": "Values",
          "action": "Written as text, with the share as a percent if asked, so nothing depends on bar length"
        }
      ],
      "customization": [
        "items and title",
        "format, showShare",
        "limit",
        "onSelect and selected"
      ]
    },
    "statesNone": "A presentational list: the items are passed in, so it has nothing of its own to load.",
    "examples": [
      "bar-list-demo",
      "bar-list-select"
    ],
    "url": "https://ui.ballmac.com/components/bar-list"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}