{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "usage-breakdown",
  "type": "registry:component",
  "title": "Usage Breakdown",
  "description": "Ranked resource consumption list with relative share and trend cues.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/usage-breakdown/usage-breakdown.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, type ReactNode, useMemo } from \"react\";\n\nimport { ArrowDownRight, ArrowUpRight } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Badge } from \"@vllnt/ui\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@vllnt/ui\";\n\nexport type UsageBreakdownTone = \"danger\" | \"default\" | \"success\" | \"warning\";\n\nexport type UsageBreakdownItem = {\n  description?: string;\n  icon?: ReactNode;\n  id: string;\n  label: string;\n  meta?: string;\n  tone?: UsageBreakdownTone;\n  trend?: {\n    direction: \"down\" | \"up\";\n    label: string;\n  };\n  value: number;\n  valueLabel?: string;\n};\n\nexport type UsageBreakdownProps = React.ComponentPropsWithoutRef<\n  typeof Card\n> & {\n  description?: string;\n  emptyMessage?: string;\n  items: UsageBreakdownItem[];\n  maxItems?: number;\n  title?: string;\n};\n\ntype UsageBreakdownRowProps = {\n  item: UsageBreakdownItem;\n  maxValue: number;\n  rank: number;\n  totalValue: number;\n};\n\nconst toneClasses: Record<UsageBreakdownTone, string> = {\n  danger:\n    \"bg-destructive/10 text-destructive border-destructive/20 dark:text-destructive\",\n  default: \"bg-muted text-muted-foreground border-border\",\n  success:\n    \"bg-emerald-500/10 text-emerald-700 border-emerald-500/20 dark:text-emerald-300\",\n  warning:\n    \"bg-amber-500/10 text-amber-700 border-amber-500/20 dark:text-amber-300\",\n};\n\nfunction formatPercent(value: number): string {\n  if (!Number.isFinite(value)) return \"0%\";\n  return `${Math.round(value)}%`;\n}\n\nconst LARGE_VALUE_FORMATTER = new Intl.NumberFormat(\"en-US\", {\n  maximumFractionDigits: 0,\n});\nconst SMALL_VALUE_FORMATTER = new Intl.NumberFormat(\"en-US\", {\n  maximumFractionDigits: 1,\n});\n\nfunction formatValue(value: number): string {\n  return value >= 100\n    ? LARGE_VALUE_FORMATTER.format(value)\n    : SMALL_VALUE_FORMATTER.format(value);\n}\n\nfunction getRelativeWidth(value: number, maxValue: number): number {\n  if (maxValue <= 0) return 0;\n  return Math.max((value / maxValue) * 100, 4);\n}\n\nfunction getShare(value: number, totalValue: number): number {\n  if (totalValue <= 0) return 0;\n  return (value / totalValue) * 100;\n}\n\nfunction UsageBreakdownTrend({ item }: { item: UsageBreakdownItem }) {\n  if (!item.trend) return null;\n\n  const trendTone = item.tone ?? \"default\";\n  const TrendIcon =\n    item.trend.direction === \"down\" ? ArrowDownRight : ArrowUpRight;\n\n  return (\n    <Badge className={cn(\"gap-1 border\", toneClasses[trendTone])}>\n      <TrendIcon className=\"size-3.5\" />\n      {item.trend.label}\n    </Badge>\n  );\n}\n\nfunction UsageBreakdownMeter({\n  maxValue,\n  value,\n}: {\n  maxValue: number;\n  value: number;\n}) {\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"h-2 overflow-hidden rounded-full bg-muted\">\n        <div\n          aria-hidden=\"true\"\n          className=\"h-full rounded-full bg-primary transition-[width]\"\n          style={{ width: `${getRelativeWidth(value, maxValue)}%` }}\n        />\n      </div>\n      <div className=\"flex items-center justify-between text-xs text-muted-foreground\">\n        <span>Relative usage</span>\n        <span>{formatPercent(getShare(value, maxValue))}</span>\n      </div>\n    </div>\n  );\n}\n\nfunction UsageBreakdownRow({\n  item,\n  maxValue,\n  rank,\n  totalValue,\n}: UsageBreakdownRowProps) {\n  return (\n    <li className=\"rounded-lg border bg-background/70 p-4\">\n      <div className=\"flex items-start gap-3\">\n        <div className=\"flex size-10 shrink-0 items-center justify-center rounded-md border bg-muted text-sm font-semibold text-muted-foreground\">\n          {item.icon ?? rank}\n        </div>\n        <div className=\"min-w-0 flex-1 space-y-3\">\n          <div className=\"flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between\">\n            <div className=\"min-w-0 space-y-1\">\n              <div className=\"flex flex-wrap items-center gap-2\">\n                <span className=\"truncate font-medium text-foreground\">\n                  {item.label}\n                </span>\n                {item.meta ? (\n                  <Badge className=\"border-border\" variant=\"outline\">\n                    {item.meta}\n                  </Badge>\n                ) : null}\n                <UsageBreakdownTrend item={item} />\n              </div>\n              {item.description ? (\n                <p className=\"text-sm text-muted-foreground\">\n                  {item.description}\n                </p>\n              ) : null}\n            </div>\n            <div className=\"text-left sm:text-right\">\n              <div className=\"font-semibold text-foreground\">\n                {item.valueLabel ?? formatValue(item.value)}\n              </div>\n              <div className=\"text-sm text-muted-foreground\">\n                {formatPercent(getShare(item.value, totalValue))} of total\n              </div>\n            </div>\n          </div>\n          <UsageBreakdownMeter maxValue={maxValue} value={item.value} />\n        </div>\n      </div>\n    </li>\n  );\n}\n\nfunction getSortedItems(items: UsageBreakdownItem[], maxItems?: number) {\n  const rankedItems = [...items].sort(\n    (left, right) => right.value - left.value,\n  );\n  return typeof maxItems === \"number\"\n    ? rankedItems.slice(0, maxItems)\n    : rankedItems;\n}\n\nconst UsageBreakdown = forwardRef<HTMLDivElement, UsageBreakdownProps>(\n  (\n    {\n      className,\n      description,\n      emptyMessage = \"No usage data available.\",\n      items,\n      maxItems,\n      title = \"Usage breakdown\",\n      ...props\n    },\n    ref,\n  ) => {\n    const sortedItems = useMemo(\n      () => getSortedItems(items, maxItems),\n      [items, maxItems],\n    );\n    const totalValue = useMemo(\n      () => sortedItems.reduce((sum, item) => sum + item.value, 0),\n      [sortedItems],\n    );\n    const maxValue = sortedItems[0]?.value ?? 0;\n\n    return (\n      <Card className={cn(\"w-full\", className)} ref={ref} {...props}>\n        <CardHeader>\n          <CardTitle>{title}</CardTitle>\n          {description ? (\n            <CardDescription>{description}</CardDescription>\n          ) : null}\n        </CardHeader>\n        <CardContent>\n          {sortedItems.length === 0 ? (\n            <div className=\"rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground\">\n              {emptyMessage}\n            </div>\n          ) : (\n            <ol className=\"space-y-3\">\n              {sortedItems.map((item, index) => (\n                <UsageBreakdownRow\n                  item={item}\n                  key={item.id}\n                  maxValue={maxValue}\n                  rank={index + 1}\n                  totalValue={totalValue}\n                />\n              ))}\n            </ol>\n          )}\n        </CardContent>\n      </Card>\n    );\n  },\n);\n\nUsageBreakdown.displayName = \"UsageBreakdown\";\n\nexport { UsageBreakdown };\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
