{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "usage-panel",
  "title": "UsagePanel",
  "description": "Per-project consumption grid composing MetricCard, with plan-gated metrics and metering-lag notice.",
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/neon-tokens.json",
    "https://ui.neon.com/r/button.json",
    "https://ui.neon.com/r/metric-card.json"
  ],
  "files": [
    {
      "path": "src/components/usage-panel/usage-panel.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport type { MetricCardProps } from \"@/components/metric-card/metric-card\";\nimport { MetricCard } from \"@/components/metric-card/metric-card\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface UsageMetric extends Pick<\n  MetricCardProps,\n  \"label\" | \"value\" | \"delta\" | \"comparisonLabel\" | \"trend\" | \"format\" | \"unit\"\n> {\n  /** Stable id, e.g. \"compute_unit_seconds\". */\n  id: string;\n  /** Locked behind a paid plan; renders the gate instead of the value. */\n  gated?: boolean;\n}\n\nexport type UsagePanelProps = Omit<ComponentProps<\"section\">, \"children\"> & {\n  /** The per-project metrics, in display order. */\n  metrics: UsageMetric[];\n  /** Billing period readout, e.g. \"Jul 1 – Jul 18\". */\n  period?: string;\n  /**\n   * Header filter slot, e.g. a DateRangePicker. Renders right-aligned;\n   * takes the period readout's place when both are set.\n   */\n  filter?: ReactNode;\n  /**\n   * Metering-lag notice: when consumption data trails real time, say\n   * so, e.g. \"metered through 21:40 UTC\". Renders as a quiet footer.\n   */\n  meteredThrough?: string;\n  /** Renders the upgrade action on gated metrics. */\n  onUpgrade?: () => void;\n  /** Skeletons every card. */\n  isLoading?: boolean;\n  /** Panel-level failure rendered above the grid. */\n  error?: string | null;\n  /** Extra content after the footer, e.g. a link to invoices. */\n  footer?: ReactNode;\n};\n\n/* ─────────────────────────────────────────────────────────\n * PANEL STORYBOARD\n *\n *  entrance  cards rise one at a time, 60ms apart, in\n *            metric order — MetricCard's own entrance\n *            rhythm, orchestrated\n *  rest      a mono header row (usage · period) over the\n *            MetricCard grid; the cards do the talking\n *  gated     a locked metric keeps the card frame but\n *            holds its voice: mono \"available on a paid\n *            plan\" where the value would be, with a quiet\n *            upgrade action — the gate is an invitation,\n *            not a wall\n *  lag       when metering trails real time, a mono footer\n *            says exactly how far — never pretend the\n *            numbers are live\n *  loading   every card skeletons through MetricCard\n *  error     the house error line above the grid; cards\n *            hold their last values\n * ───────────────────────────────────────────────────────── */\nconst CARD_STAGGER_MS = 60;\n\nconst RISE =\n  \"fill-mode-backwards fade-in-0 slide-in-from-bottom-2 animate-in duration-500 motion-reduce:animate-none\";\n\n/** The locked metric: same frame, held voice, an invitation. */\nconst GatedCard = ({\n  label,\n  onUpgrade,\n}: {\n  label: string;\n  onUpgrade?: () => void;\n}) => (\n  <div\n    className=\"flex min-h-[168px] flex-col overflow-hidden rounded-lg border border-border/60 bg-card transition-colors hover:border-border\"\n    data-slot=\"usage-panel-gated\"\n  >\n    <p\n      className=\"truncate px-4 pt-4 font-medium font-mono text-muted-foreground text-xs\"\n      title={label}\n    >\n      {label}\n    </p>\n    <div className=\"flex flex-1 flex-col items-center justify-center gap-3 px-4 pb-4\">\n      <p className=\"text-[10px] text-muted-foreground/70\">\n        available on a paid plan\n      </p>\n      {onUpgrade ? (\n        <Button\n          className=\"h-6 rounded-full border border-border/60 px-2.5 text-muted-foreground text-xs transition-colors hover:border-primary/60 hover:bg-transparent hover:text-primary active:scale-[0.98]\"\n          onClick={onUpgrade}\n          size=\"sm\"\n          variant=\"ghost\"\n        >\n          Upgrade\n        </Button>\n      ) : null}\n    </div>\n  </div>\n);\n\nexport const UsagePanel = ({\n  className,\n  error = null,\n  filter,\n  footer,\n  isLoading = false,\n  meteredThrough,\n  metrics,\n  onUpgrade,\n  period,\n  ...props\n}: UsagePanelProps) => (\n  <section\n    className={cn(\"flex w-full flex-col gap-3\", className)}\n    data-slot=\"usage-panel\"\n    {...props}\n  >\n    <div className=\"flex items-center justify-between gap-3\">\n      <h3 className=\"font-mono text-muted-foreground text-xs\">usage</h3>\n      {filter ??\n        (period ? (\n          <span className=\"font-mono text-[10px] text-muted-foreground/70 tabular-nums\">\n            {period}\n          </span>\n        ) : null)}\n    </div>\n\n    {error ? (\n      <p\n        className=\"flex items-baseline gap-2 text-sm\"\n        data-slot=\"usage-panel-error\"\n        role=\"alert\"\n      >\n        <span className=\"font-mono text-destructive text-xs\">error</span>\n        <span className=\"text-foreground\">{error}</span>\n      </p>\n    ) : null}\n\n    <div className=\"grid gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n      {metrics.map(({ gated, id, ...metric }, index) => (\n        <div\n          className={RISE}\n          key={id}\n          style={{ animationDelay: `${index * CARD_STAGGER_MS}ms` }}\n        >\n          {gated ? (\n            <GatedCard label={metric.label} onUpgrade={onUpgrade} />\n          ) : (\n            <MetricCard isLoading={isLoading} {...metric} />\n          )}\n        </div>\n      ))}\n    </div>\n\n    {meteredThrough ? (\n      <p\n        className=\"text-[10px] text-muted-foreground/70 tabular-nums\"\n        data-slot=\"usage-panel-lag\"\n      >\n        {meteredThrough}\n      </p>\n    ) : null}\n    {footer}\n  </section>\n);\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}