{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bar-list",
  "type": "registry:ui",
  "title": "Bar List",
  "description": "Ranked horizontal bars with label, compact value and optional icon, link or button rows, stable sorting, a show-more limit, chart-palette colors and reduced-motion-safe entrance.",
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json",
    "https://components.dethink.co.uk/r/chart-core.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/bar-list/bar-list.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/bar-list/bar-list.tsx",
      "content": "import {\n  forwardRef,\n  useId,\n  useState,\n  type CSSProperties,\n  type HTMLAttributes,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport {\n  formatChartValue,\n  isFiniteValue,\n  resolveChartColor,\n  type ChartColor,\n  type FormatChartValueOptions,\n} from \"../chart/chart-core\";\n\nexport type BarListSort = \"descending\" | \"ascending\" | \"none\";\nexport type BarListSize = \"sm\" | \"md\";\n\nexport interface BarListItem {\n  /** Stable identity. Defaults to the label when it is a string, then the index. */\n  key?: string;\n  label: ReactNode;\n  value: number;\n  /** Render the row as a link. */\n  href?: string;\n  /** Small leading visual such as a favicon, flag or icon. */\n  icon?: ReactNode;\n  /** Override the bar color for this row (identity, never rank). */\n  color?: ChartColor;\n}\n\nexport interface BarListProps extends Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\" | \"color\"\n> {\n  data: readonly BarListItem[];\n  /** Ties keep their input order. */\n  sort?: BarListSort;\n  /** Rows shown before a \"Show more\" toggle. */\n  limit?: number;\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n  /** Bar color for every row. */\n  color?: ChartColor;\n  /** Value that fills the track. Defaults to the largest value. */\n  max?: number;\n  formatValue?: (value: number) => string;\n  formatOptions?: FormatChartValueOptions;\n  /** Column headings, for example \"Page\" and \"Visitors\". */\n  labelHeader?: ReactNode;\n  valueHeader?: ReactNode;\n  /** Makes rows without an href into buttons. */\n  onItemClick?: (item: BarListItem, index: number) => void;\n  emptyLabel?: ReactNode;\n  showMoreLabel?: (hiddenCount: number) => ReactNode;\n  showLessLabel?: ReactNode;\n  size?: BarListSize;\n  animate?: boolean;\n}\n\nconst rowHeightClasses: Record<BarListSize, string> = {\n  sm: \"min-h-7 text-xs\",\n  md: \"min-h-8 text-sm\",\n};\n\nconst barHeightClasses: Record<BarListSize, string> = {\n  sm: \"h-5\",\n  md: \"h-6\",\n};\n\n// A 24px-or-thinner tinted bar: the baseline end stays square and the data end\n// is rounded 4px. A tint (not a solid fill) keeps labels on top at text contrast.\nconst barClasses =\n  // max() keeps a sliver visible for tiny non-zero values.\n  \"pointer-events-none absolute w-[max(4px,var(--bar-share))] inset-y-0 start-0 m-auto rounded-e-[4px] bg-[color-mix(in_oklab,var(--bar-color)_26%,transparent)] [transform-origin:left] rtl:[transform-origin:right] motion-safe:animate-[dt-chart-grow-x_700ms_var(--ease-control)_both]\";\n\nconst interactiveRowClasses =\n  \"rounded-md outline-hidden hover:bg-muted/60 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-ring motion-safe:transition-colors\";\n\nfunction itemKey(item: BarListItem, index: number) {\n  if (item.key !== undefined) return item.key;\n  return typeof item.label === \"string\" ? `${item.label}` : String(index);\n}\n\n/** Sort without reordering ties, so equal values keep their input order. */\nexport function sortBarListItems(\n  data: readonly BarListItem[],\n  sort: BarListSort = \"descending\",\n) {\n  const indexed = data.map((item, index) => ({ item, index }));\n  if (sort !== \"none\") {\n    const direction = sort === \"descending\" ? -1 : 1;\n    indexed.sort(\n      (a, b) =>\n        direction * ((a.item.value || 0) - (b.item.value || 0)) ||\n        a.index - b.index,\n    );\n  }\n  return indexed;\n}\n\nexport const BarList = forwardRef<HTMLDivElement, BarListProps>(\n  (\n    {\n      animate = true,\n      \"aria-describedby\": ariaDescribedBy,\n      \"aria-label\": ariaLabel,\n      \"aria-labelledby\": ariaLabelledBy,\n      className,\n      color = \"chart-1\",\n      data,\n      defaultExpanded = false,\n      emptyLabel = \"No data\",\n      expanded: expandedProp,\n      formatOptions,\n      formatValue,\n      labelHeader,\n      limit,\n      max,\n      onExpandedChange,\n      onItemClick,\n      showLessLabel = \"Show less\",\n      showMoreLabel = (hidden) => `Show ${hidden} more`,\n      size = \"md\",\n      sort = \"descending\",\n      valueHeader,\n      ...props\n    },\n    ref,\n  ) => {\n    const listId = useId();\n    const [expandedState, setExpandedState] = useState(defaultExpanded);\n    const expanded = expandedProp ?? expandedState;\n    const setExpanded = (next: boolean) => {\n      if (expandedProp === undefined) setExpandedState(next);\n      onExpandedChange?.(next);\n    };\n\n    const rows = sortBarListItems(data, sort);\n    const collapsible =\n      limit !== undefined && limit >= 0 && rows.length > limit;\n    const visible = collapsible && !expanded ? rows.slice(0, limit) : rows;\n    const scaleMax =\n      max ??\n      Math.max(\n        0,\n        ...data.map((item) => (isFiniteValue(item.value) ? item.value : 0)),\n      );\n    const format = (value: number) =>\n      formatValue ? formatValue(value) : formatChartValue(value, formatOptions);\n    // The list carries the name; a generic <div> cannot be named, so the empty\n    // state falls back to a named group on the root.\n    const labelling = {\n      \"aria-describedby\": ariaDescribedBy,\n      \"aria-label\": ariaLabel,\n      \"aria-labelledby\": ariaLabelledBy,\n    };\n    const hasLabelling =\n      ariaDescribedBy !== undefined ||\n      ariaLabel !== undefined ||\n      ariaLabelledBy !== undefined;\n    const labelRoot = rows.length === 0 && hasLabelling;\n\n    return (\n      <div\n        role={labelRoot ? \"group\" : undefined}\n        {...props}\n        {...(labelRoot ? labelling : undefined)}\n        ref={ref}\n        data-slot=\"bar-list\"\n        data-size={size}\n        className={cn(\"grid min-w-0 gap-[var(--dt-space-2)]\", className)}\n      >\n        {labelHeader || valueHeader ? (\n          <div\n            data-slot=\"bar-list-header\"\n            className=\"text-muted-foreground flex items-center justify-between gap-[var(--dt-space-4)] px-[var(--dt-space-2)] text-xs font-medium\"\n          >\n            <span>{labelHeader}</span>\n            <span>{valueHeader}</span>\n          </div>\n        ) : null}\n\n        {rows.length === 0 ? (\n          <p\n            data-slot=\"bar-list-empty\"\n            className=\"text-muted-foreground px-[var(--dt-space-2)] py-[var(--dt-space-3)] text-sm\"\n          >\n            {emptyLabel}\n          </p>\n        ) : (\n          // Rows subgrid into a shared value column, so the widest value sets\n          // the track and every bar is measured against the same length.\n          <ul\n            {...labelling}\n            id={listId}\n            className=\"grid grid-cols-[minmax(0,1fr)_auto] gap-x-[var(--dt-space-4)] gap-y-[var(--dt-space-1-5)]\"\n          >\n            {visible.map(({ item, index }, position) => {\n              const value = isFiniteValue(item.value) ? item.value : 0;\n              const share =\n                scaleMax > 0 ? Math.min(1, Math.max(0, value / scaleMax)) : 0;\n              const rowStyle = {\n                \"--bar-color\": resolveChartColor(item.color ?? color),\n              } as CSSProperties;\n              const interactive = item.href !== undefined || onItemClick;\n              const content = (\n                <>\n                  <span className=\"relative flex min-w-0 items-center\">\n                    {share > 0 ? (\n                      <span\n                        aria-hidden=\"true\"\n                        data-slot=\"bar-list-bar\"\n                        className={cn(barClasses, barHeightClasses[size])}\n                        style={{\n                          [\"--bar-share\" as string]: `${(share * 100).toFixed(2)}%`,\n                          animation: animate ? undefined : \"none\",\n                          animationDelay: animate\n                            ? `${Math.min(position, 12) * 30}ms`\n                            : undefined,\n                        }}\n                      />\n                    ) : null}\n                    <span className=\"relative flex min-w-0 items-center gap-[var(--dt-space-2)] px-[var(--dt-space-2)]\">\n                      {item.icon ? (\n                        <span\n                          aria-hidden=\"true\"\n                          className=\"inline-flex size-4 shrink-0 items-center justify-center [&>img]:size-4 [&>svg]:size-4\"\n                        >\n                          {item.icon}\n                        </span>\n                      ) : null}\n                      <span\n                        data-slot=\"bar-list-label\"\n                        // Labels follow their own script, so \"/pricing\" never\n                        // renders as \"pricing/\" in right-to-left layouts.\n                        dir=\"auto\"\n                        className=\"min-w-0 truncate\"\n                        title={\n                          typeof item.label === \"string\"\n                            ? item.label\n                            : undefined\n                        }\n                      >\n                        {item.label}\n                      </span>\n                    </span>\n                  </span>\n                  <span\n                    data-slot=\"bar-list-value\"\n                    dir=\"ltr\"\n                    className=\"text-foreground justify-self-end font-medium tabular-nums\"\n                  >\n                    {format(value)}\n                  </span>\n                </>\n              );\n              const rowClasses = cn(\n                \"col-span-2 grid w-full min-w-0 grid-cols-subgrid items-center pe-[var(--dt-space-2)] text-start text-foreground\",\n                rowHeightClasses[size],\n                interactive && interactiveRowClasses,\n              );\n\n              return (\n                <li\n                  key={itemKey(item, index)}\n                  style={rowStyle}\n                  data-slot=\"bar-list-item\"\n                  className=\"col-span-2 grid grid-cols-subgrid\"\n                >\n                  {item.href !== undefined ? (\n                    <a\n                      href={item.href}\n                      className={rowClasses}\n                      onClick={\n                        onItemClick ? () => onItemClick(item, index) : undefined\n                      }\n                    >\n                      {content}\n                    </a>\n                  ) : onItemClick ? (\n                    <button\n                      type=\"button\"\n                      className={rowClasses}\n                      onClick={() => onItemClick(item, index)}\n                    >\n                      {content}\n                    </button>\n                  ) : (\n                    <div className={rowClasses}>{content}</div>\n                  )}\n                </li>\n              );\n            })}\n          </ul>\n        )}\n\n        {collapsible ? (\n          <button\n            type=\"button\"\n            data-slot=\"bar-list-toggle\"\n            aria-expanded={expanded}\n            aria-controls={listId}\n            onClick={() => setExpanded(!expanded)}\n            className=\"text-muted-foreground hover:text-foreground focus-visible:outline-ring justify-self-start rounded-sm px-[var(--dt-space-2)] py-[var(--dt-space-1)] text-xs font-medium outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid\"\n          >\n            {expanded\n              ? showLessLabel\n              : showMoreLabel(rows.length - (limit ?? 0))}\n          </button>\n        ) : null}\n      </div>\n    );\n  },\n);\n\nBarList.displayName = \"BarList\";\n"
    },
    {
      "path": "packages/components/src/components/bar-list/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/bar-list/index.ts",
      "content": "export {\n  BarList,\n  sortBarListItems,\n  type BarListItem,\n  type BarListProps,\n  type BarListSize,\n  type BarListSort,\n} from \"./bar-list\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
