{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "usage-breakdown",
  "title": "Usage Breakdown",
  "description": "Ranked resource consumption list with relative share and trend cues.",
  "dependencies": [
    "@vllnt/ui@^0.3.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/usage-breakdown/usage-breakdown.tsx",
      "content": "\"use client\";\n\nimport { 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 = ({\n  className,\n  description,\n  emptyMessage = \"No usage data available.\",\n  items,\n  maxItems,\n  ref,\n  title = \"Usage breakdown\",\n  ...props\n}: UsageBreakdownProps & { ref?: React.Ref<HTMLDivElement> }) => {\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 ? <CardDescription>{description}</CardDescription> : 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\nUsageBreakdown.displayName = \"UsageBreakdown\";\n\nexport { UsageBreakdown };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
