{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"usage-dashboard","type":"registry:component","title":"Usage Dashboard","description":"A coding-agent usage overview with daily token trends, provider and model totals, billed spend, and a daily breakdown.","author":"AgentUI","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/agents/usage-dashboard.tsx","type":"registry:component","target":"@components/agents/usage-dashboard.tsx","content":"\"use client\";\n// www.agentui.pro/components/agents/usage-dashboard\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { type KeyboardEvent, type PointerEvent, useId, useMemo, useState } from \"react\";\nimport { SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  summarizeUsage,\n  type DailyUsage,\n  type UsageBreakdown,\n  type UsageRecord,\n  type UsageTotals,\n} from \"./usage-dashboard-data\";\nimport { UsageProviderMark } from \"./usage-provider-mark\";\n\nexport type { UsageRecord } from \"./usage-dashboard-data\";\n\nexport interface UsageDashboardProps {\n  records: UsageRecord[];\n  title?: string;\n  /** Controlled range, in UTC calendar days. */\n  range?: 7 | 30;\n  defaultRange?: 7 | 30;\n  onRangeChange?: (range: 7 | 30) => void;\n  className?: string;\n}\n\ntype AgentSeries = { name: string; values: number[] };\n\nconst seriesColors = [\n  \"var(--usage-first)\",\n  \"var(--usage-second)\",\n  \"var(--usage-third)\",\n  \"var(--usage-fourth)\",\n];\nconst compact = new Intl.NumberFormat(\"en-US\", {\n  notation: \"compact\",\n  maximumFractionDigits: 1,\n});\nconst integer = new Intl.NumberFormat(\"en-US\");\nconst money = new Intl.NumberFormat(\"en-US\", {\n  style: \"currency\",\n  currency: \"USD\",\n  minimumFractionDigits: 2,\n  maximumFractionDigits: 2,\n});\n\nfunction formatMoney(value: number) {\n  return value > 0 && value < 0.01 ? \"<$0.01\" : money.format(value);\n}\n\nfunction formatDate(date: string, options: Intl.DateTimeFormatOptions) {\n  return new Intl.DateTimeFormat(\"en-US\", { timeZone: \"UTC\", ...options }).format(\n    new Date(`${date}T00:00:00.000Z`),\n  );\n}\n\nfunction tokenCount(totals: UsageTotals) {\n  return totals.inputTokens + totals.outputTokens;\n}\n\nfunction chartPath(values: number[], max: number) {\n  return values\n    .map((value, index) => {\n      const x = 46 + (index / Math.max(values.length - 1, 1)) * 684;\n      const y = 22 + 178 * (1 - value / max);\n      return `${index === 0 ? \"M\" : \"L\"}${x.toFixed(2)} ${y.toFixed(2)}`;\n    })\n    .join(\" \");\n}\n\nfunction AgentChart({\n  daily,\n  series,\n}: {\n  daily: DailyUsage[];\n  series: AgentSeries[];\n}) {\n  const gradientId = useId().replaceAll(\":\", \"\");\n  const [activeIndex, setActiveIndex] = useState<number | null>(null);\n  const peak = Math.max(1, ...series.flatMap((item) => item.values));\n  const step = 10 ** Math.floor(Math.log10(peak));\n  const max = Math.ceil((peak * 1.16) / step) * step;\n  const activeDay = activeIndex === null ? null : daily[activeIndex];\n  const activeX = activeIndex === null ? 0 : 46 + (activeIndex / Math.max(daily.length - 1, 1)) * 684;\n  const labels = [0, Math.floor((daily.length - 1) / 2), daily.length - 1];\n\n  const inspectPointer = (event: PointerEvent<HTMLButtonElement>) => {\n    const bounds = event.currentTarget.getBoundingClientRect();\n    const plotX = ((event.clientX - bounds.left) / bounds.width) * 760;\n    const index = Math.round(((plotX - 46) / 684) * (daily.length - 1));\n    setActiveIndex(Math.max(0, Math.min(daily.length - 1, index)));\n  };\n\n  const inspectKeyboard = (event: KeyboardEvent<HTMLButtonElement>) => {\n    if (![\"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"].includes(event.key)) return;\n    event.preventDefault();\n    setActiveIndex((previous) => {\n      if (event.key === \"Home\") return 0;\n      if (event.key === \"End\") return daily.length - 1;\n      const current = previous ?? daily.length - 1;\n      return Math.max(0, Math.min(daily.length - 1, current + (event.key === \"ArrowRight\" ? 1 : -1)));\n    });\n  };\n\n  return (\n    <div className=\"min-w-0\">\n      <div className=\"flex min-h-20 items-start justify-between gap-3\">\n        <div className=\"min-w-0\">\n          <h3 className=\"text-sm font-semibold text-foreground\">Daily processed tokens</h3>\n          <div className=\"mt-2 flex flex-wrap gap-x-4 gap-y-1.5\">\n            {series.map((item, index) => (\n              <span key={item.name} className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground\">\n                <span className=\"size-2 rounded-full\" style={{ backgroundColor: seriesColors[index % seriesColors.length] }} />\n                {item.name}\n              </span>\n            ))}\n          </div>\n        </div>\n        <div className=\"min-w-28 shrink-0 text-right text-xs tabular-nums\" aria-live=\"polite\">\n          {activeDay ? (\n            <div className=\"rounded-lg border border-border bg-popover px-2.5 py-2 text-popover-foreground shadow-sm\">\n              <p className=\"font-medium\">{formatDate(activeDay.date, { month: \"short\", day: \"numeric\" })}</p>\n              {series.map((item) => (\n                <p key={item.name} className=\"mt-0.5 flex items-center justify-between gap-3 text-muted-foreground\">\n                  <span className=\"truncate\">{item.name}</span>\n                  <span className=\"shrink-0 text-popover-foreground\">{compact.format(item.values[activeIndex ?? 0])}</span>\n                </p>\n              ))}\n            </div>\n          ) : (\n            <p className=\"pt-1 text-muted-foreground\">Inspect a day</p>\n          )}\n        </div>\n      </div>\n      <div className=\"relative mt-2 w-full\">\n        <button\n          type=\"button\"\n          className=\"absolute inset-0 z-10 w-full cursor-crosshair rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n          aria-label=\"Daily token chart. Move the pointer or use left and right arrow keys to inspect a day.\"\n          onPointerMove={inspectPointer}\n          onPointerDown={inspectPointer}\n          onPointerLeave={(event) => { if (event.pointerType === \"mouse\") setActiveIndex(null); }}\n          onFocus={() => setActiveIndex((previous) => previous ?? daily.length - 1)}\n          onBlur={() => setActiveIndex(null)}\n          onKeyDown={inspectKeyboard}\n        />\n        <svg viewBox=\"0 0 760 235\" className=\"block w-full\" aria-hidden=\"true\">\n          <defs>\n            <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n              <stop offset=\"0\" stopColor=\"var(--usage-first)\" stopOpacity=\"0.14\" />\n              <stop offset=\"1\" stopColor=\"var(--usage-first)\" stopOpacity=\"0\" />\n            </linearGradient>\n          </defs>\n          {[0, 0.5, 1].map((fraction) => {\n            const y = 200 - fraction * 178;\n            return (\n              <g key={fraction}>\n                <line x1=\"46\" x2=\"730\" y1={y} y2={y} className=\"stroke-border/80\" strokeDasharray={fraction === 0 ? undefined : \"3 5\"} />\n                <text x=\"38\" y={y + 4} textAnchor=\"end\" className=\"fill-muted-foreground text-[10px] tabular-nums\">{compact.format(max * fraction)}</text>\n              </g>\n            );\n          })}\n          {series[0] && <path d={`${chartPath(series[0].values, max)} L730 200 L46 200 Z`} fill={`url(#${gradientId})`} />}\n          {series.map((item, index) => (\n            <path key={item.name} d={chartPath(item.values, max)} fill=\"none\" stroke={seriesColors[index % seriesColors.length]} strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" vectorEffect=\"non-scaling-stroke\" />\n          ))}\n          {activeDay && (\n            <g>\n              <line x1={activeX} x2={activeX} y1=\"22\" y2=\"200\" className=\"stroke-foreground/30\" strokeDasharray=\"3 4\" />\n              {series.map((item, index) => (\n                <circle key={item.name} cx={activeX} cy={22 + 178 * (1 - item.values[activeIndex ?? 0] / max)} r=\"4.5\" fill={seriesColors[index % seriesColors.length]} stroke=\"var(--usage-surface)\" strokeWidth=\"2\" />\n              ))}\n            </g>\n          )}\n          {labels.map((index, labelIndex) => (\n            <text key={index} x={46 + (index / Math.max(daily.length - 1, 1)) * 684} y=\"230\" textAnchor={labelIndex === 0 ? \"start\" : labelIndex === 2 ? \"end\" : \"middle\"} className=\"fill-muted-foreground text-[10px]\">\n              {formatDate(daily[index].date, { month: \"short\", day: \"numeric\" })}\n            </text>\n          ))}\n        </svg>\n      </div>\n      <p className=\"sr-only\">Use the Day tab in Breakdown for exact daily values.</p>\n    </div>\n  );\n}\n\nfunction AgentSummary({ agents, totals }: { agents: UsageBreakdown[]; totals: UsageTotals }) {\n  return (\n    <div className=\"min-w-0\">\n      <p className=\"text-xs font-medium text-muted-foreground\">Processed tokens</p>\n      <p className=\"mt-1 text-[clamp(2.6rem,7vw,4.1rem)] font-semibold leading-none tracking-[-0.07em] tabular-nums text-foreground\">{compact.format(tokenCount(totals))}</p>\n      <p className=\"mt-2 text-xs text-muted-foreground\">Across {integer.format(totals.requests)} requests</p>\n      <ul className=\"mt-7 space-y-4\">\n        {agents.map((agent, index) => (\n          <li key={`${agent.name}\\0${agent.provider}`} className=\"border-t border-border pt-3\">\n            <div className=\"flex items-center justify-between gap-3 text-sm\">\n              <span className=\"flex min-w-0 items-center gap-2 font-medium text-foreground\"><span className=\"size-2 shrink-0 rounded-full\" style={{ backgroundColor: seriesColors[index % seriesColors.length] }} /><UsageProviderMark provider={agent.provider} className=\"size-[18px] text-foreground\" /><span className=\"truncate\">{agent.name}</span></span>\n              <span className=\"shrink-0 font-medium tabular-nums text-foreground\">{compact.format(tokenCount(agent))}</span>\n            </div>\n            <div className=\"mt-1 flex items-center justify-between gap-3 pl-4 text-xs text-muted-foreground\">\n              <span className=\"truncate\">{agent.provider} · {Math.round((tokenCount(agent) / Math.max(tokenCount(totals), 1)) * 100)}%</span>\n              <span className=\"shrink-0 tabular-nums\">{formatMoney(agent.costUsd)}</span>\n            </div>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\nfunction Total({ label, value }: { label: string; value: string }) {\n  return (\n    <div className=\"min-w-0\">\n      <dt className=\"text-xs text-muted-foreground\">{label}</dt>\n      <dd className=\"mt-1.5 text-xl font-semibold tracking-[-0.04em] tabular-nums text-foreground sm:text-2xl\">{value}</dd>\n    </div>\n  );\n}\n\nfunction Breakdown({\n  models,\n  daily,\n  totals,\n  reduceMotion,\n  layoutId,\n  view,\n  onViewChange,\n}: {\n  models: UsageBreakdown[];\n  daily: DailyUsage[];\n  totals: UsageTotals;\n  reduceMotion: boolean;\n  layoutId: string;\n  view: \"model\" | \"day\";\n  onViewChange: (view: \"model\" | \"day\") => void;\n}) {\n  const [dayPage, setDayPage] = useState(0);\n  const pageCount = Math.ceil(daily.length / 7);\n  const pageIndex = Math.min(dayPage, pageCount - 1);\n  const pageEnd = daily.length - pageIndex * 7;\n  const visibleDays = daily.slice(Math.max(0, pageEnd - 7), pageEnd);\n\n  return (\n    <div className=\"pt-7\">\n      <div className=\"flex flex-wrap items-center justify-between gap-4\">\n        <h3 className=\"text-base font-semibold tracking-tight text-foreground\">Breakdown</h3>\n        <fieldset className=\"inline-flex rounded-full border border-border bg-muted/50 p-1\">\n          <legend className=\"sr-only\">Breakdown view</legend>\n          {([\"model\", \"day\"] as const).map((option) => (\n            <button\n              key={option}\n              type=\"button\"\n              aria-pressed={view === option}\n              onClick={() => onViewChange(option)}\n              className={cn(\"relative isolate min-h-8 rounded-full px-3.5 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\", view === option ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\")}\n            >\n              {view === option && <motion.span layoutId={layoutId} className=\"absolute inset-0 -z-10 rounded-full border border-border/70 bg-background shadow-sm\" transition={reduceMotion ? { duration: 0 } : SPRING_LAYOUT} />}\n              {option === \"model\" ? \"Model\" : \"Day\"}\n            </button>\n          ))}\n        </fieldset>\n      </div>\n      <div>\n          {view === \"model\" ? (\n            <table className=\"mt-5 w-full border-collapse text-left text-xs sm:text-sm\">\n              <thead><tr className=\"border-b border-border text-muted-foreground\"><th scope=\"col\" className=\"pb-3 font-medium\">Model</th><th scope=\"col\" className=\"hidden pb-3 text-right font-medium sm:table-cell\">Share</th><th scope=\"col\" className=\"pb-3 text-right font-medium\">Cost</th><th scope=\"col\" className=\"pb-3 text-right font-medium\">Tokens</th></tr></thead>\n              <tbody>{models.map((model) => (\n                <tr key={`${model.provider}\\0${model.name}`} className=\"border-b border-border/65 last:border-0\">\n                  <th scope=\"row\" className=\"min-w-0 py-3 pr-2 font-medium text-foreground\"><span className=\"flex items-center gap-2\"><UsageProviderMark provider={model.provider} className=\"size-4 text-foreground\" /><span className=\"break-all\">{model.name}</span></span><span className=\"block font-normal text-muted-foreground\">{model.provider}</span></th>\n                  <td className=\"hidden py-3 text-right tabular-nums text-muted-foreground sm:table-cell\">{Math.round((tokenCount(model) / Math.max(tokenCount(totals), 1)) * 100)}%</td>\n                  <td className=\"py-3 text-right tabular-nums text-muted-foreground\">{formatMoney(model.costUsd)}</td>\n                  <td className=\"py-3 text-right font-medium tabular-nums text-foreground\">{compact.format(tokenCount(model))}</td>\n                </tr>\n              ))}</tbody>\n            </table>\n          ) : (\n            <div className=\"mt-5\">\n              <table className=\"w-full border-collapse text-left text-xs sm:text-sm\">\n                <thead className=\"sticky top-0 bg-card\"><tr className=\"border-b border-border text-muted-foreground\"><th scope=\"col\" className=\"pb-3 font-medium\">Day · UTC</th><th scope=\"col\" className=\"hidden pb-3 text-right font-medium sm:table-cell\">Input</th><th scope=\"col\" className=\"hidden pb-3 text-right font-medium sm:table-cell\">Output</th><th scope=\"col\" className=\"pb-3 text-right font-medium\">Cost</th><th scope=\"col\" className=\"pb-3 text-right font-medium\">Tokens</th></tr></thead>\n                <tbody>{visibleDays.map((day) => (\n                  <tr key={day.date} className=\"border-b border-border/65 last:border-0\">\n                    <th scope=\"row\" className=\"py-3 font-medium text-foreground\">{formatDate(day.date, { month: \"short\", day: \"numeric\" })}</th>\n                    <td className=\"hidden py-3 text-right tabular-nums text-muted-foreground sm:table-cell\">{integer.format(day.inputTokens)}</td>\n                    <td className=\"hidden py-3 text-right tabular-nums text-muted-foreground sm:table-cell\">{integer.format(day.outputTokens)}</td>\n                    <td className=\"py-3 text-right tabular-nums text-muted-foreground\">{formatMoney(day.costUsd)}</td>\n                    <td className=\"py-3 text-right font-medium tabular-nums text-foreground\">{compact.format(tokenCount(day))}</td>\n                  </tr>\n                ))}</tbody>\n              </table>\n              {pageCount > 1 && (\n                <div className=\"mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4\">\n                  <p className=\"text-xs text-muted-foreground\">\n                    {formatDate(visibleDays[0].date, { month: \"short\", day: \"numeric\" })} – {formatDate(visibleDays[visibleDays.length - 1].date, { month: \"short\", day: \"numeric\" })} · {visibleDays.length} of {daily.length} days\n                  </p>\n                  <div className=\"flex items-center gap-2\">\n                    <button\n                      type=\"button\"\n                      disabled={pageIndex >= pageCount - 1}\n                      onClick={() => setDayPage(pageIndex + 1)}\n                      className=\"min-h-10 rounded-full border border-border px-3 text-xs font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40\"\n                    >\n                      ← Older\n                    </button>\n                    <button\n                      type=\"button\"\n                      disabled={pageIndex === 0}\n                      onClick={() => setDayPage(pageIndex - 1)}\n                      className=\"min-h-10 rounded-full border border-border px-3 text-xs font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40\"\n                    >\n                      Newer →\n                    </button>\n                  </div>\n                </div>\n              )}\n            </div>\n          )}\n      </div>\n    </div>\n  );\n}\n\n/** Provider-billed usage overview. Pass real per-day records; the component never estimates prices. */\nexport function UsageDashboard({\n  records,\n  title = \"Usage overview\",\n  range,\n  defaultRange = 7,\n  onRangeChange,\n  className,\n}: UsageDashboardProps) {\n  const reduceMotion = useReducedMotion() ?? false;\n  const rangeId = useId();\n  const breakdownId = useId();\n  const [localRange, setLocalRange] = useState<7 | 30>(defaultRange);\n  const [breakdownView, setBreakdownView] = useState<\"model\" | \"day\">(\"model\");\n  const selectedRange = range ?? localRange;\n  const usage = useMemo(() => summarizeUsage(records, selectedRange), [records, selectedRange]);\n\n  const changeRange = (next: 7 | 30) => {\n    if (range === undefined) setLocalRange(next);\n    onRangeChange?.(next);\n  };\n\n  return (\n    <section\n      className={cn(\n        \"w-full max-w-5xl rounded-[1.35rem] border border-border bg-card p-4 text-card-foreground [--usage-first:#0e857e] [--usage-second:#7468bd] [--usage-third:#bd8562] [--usage-fourth:#6b9aad] [--usage-surface:#fff] dark:[--usage-first:#65d5c3] dark:[--usage-second:#b6a6f7] dark:[--usage-third:#d9a778] dark:[--usage-fourth:#87bdca] dark:[--usage-surface:#151515] sm:p-7\",\n        className,\n      )}\n      aria-label={title}\n    >\n      <div className=\"flex flex-wrap items-start justify-between gap-5\">\n        <div>\n          <h2 className=\"text-2xl font-semibold tracking-[-0.045em] text-foreground sm:text-3xl\">{title}</h2>\n          <p className=\"mt-1.5 text-sm text-muted-foreground\">\n            {usage\n              ? `${formatDate(usage.startDate, { month: \"short\", day: \"numeric\" })} – ${formatDate(usage.endDate, { month: \"short\", day: \"numeric\", year: \"numeric\" })} · UTC`\n              : \"No usage data yet\"}\n          </p>\n        </div>\n        <fieldset className=\"inline-flex rounded-full border border-border bg-muted/50 p-1\">\n          <legend className=\"sr-only\">Usage period</legend>\n          {([7, 30] as const).map((days) => (\n            <button\n              key={days}\n              type=\"button\"\n              aria-pressed={selectedRange === days}\n              onClick={() => changeRange(days)}\n              className={cn(\"relative isolate min-h-9 rounded-full px-3.5 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\", selectedRange === days ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\")}\n            >\n              {selectedRange === days && <motion.span layoutId={`${rangeId}-range`} className=\"absolute inset-0 -z-10 rounded-full border border-border/70 bg-background shadow-sm\" transition={reduceMotion ? { duration: 0 } : SPRING_LAYOUT} />}\n              {days} days\n            </button>\n          ))}\n        </fieldset>\n      </div>\n\n      {!usage || usage.recordCount === 0 ? (\n        <div className=\"mt-8 flex min-h-64 flex-col items-center justify-center rounded-xl border border-dashed border-border px-5 text-center\">\n          <p className=\"text-sm font-medium text-foreground\">No usage in this period</p>\n          <p className=\"mt-1 max-w-sm text-sm text-muted-foreground\">Pass daily, provider-billed records to see agent and model usage.</p>\n        </div>\n      ) : (\n        <div>\n            <div className=\"mt-7 grid gap-8 border-b border-border pb-7 lg:grid-cols-[minmax(200px,0.85fr)_minmax(0,2fr)] lg:gap-10\">\n              <AgentSummary agents={usage.agents} totals={usage.totals} />\n              <AgentChart daily={usage.daily} series={usage.agentSeries} />\n            </div>\n            <div className=\"border-b border-border py-7\">\n              <h3 className=\"text-sm font-semibold text-foreground\">Totals</h3>\n              <dl className=\"mt-5 grid grid-cols-2 gap-x-5 gap-y-6 sm:grid-cols-4\">\n                <Total label=\"Cached input\" value={compact.format(usage.totals.cachedInputTokens)} />\n                <Total label=\"Uncached input\" value={compact.format(usage.totals.inputTokens - usage.totals.cachedInputTokens)} />\n                <Total label=\"Output\" value={compact.format(usage.totals.outputTokens)} />\n                <Total label=\"Billed spend · USD\" value={formatMoney(usage.totals.costUsd)} />\n              </dl>\n            </div>\n            <Breakdown key={selectedRange} models={usage.models} daily={usage.daily} totals={usage.totals} reduceMotion={reduceMotion} layoutId={`${breakdownId}-view`} view={breakdownView} onViewChange={setBreakdownView} />\n        </div>\n      )}\n    </section>\n  );\n}\n"},{"path":"components/agents/usage-dashboard-data.ts","type":"registry:component","target":"@components/agents/usage-dashboard-data.ts","content":"export interface UsageRecord {\n  /** UTC calendar day in YYYY-MM-DD format. */\n  date: string;\n  provider: string;\n  model: string;\n  /** Optional coding agent or client, such as Codex or Claude Code. */\n  agent?: string;\n  /** Input includes cached input; do not add cachedInputTokens to it. */\n  inputTokens: number;\n  cachedInputTokens?: number;\n  outputTokens: number;\n  requests: number;\n  /** Billed amount from the provider, in USD. No price is inferred. */\n  costUsd: number;\n}\n\nexport interface UsageTotals {\n  inputTokens: number;\n  cachedInputTokens: number;\n  outputTokens: number;\n  requests: number;\n  costUsd: number;\n}\n\nexport interface DailyUsage extends UsageTotals {\n  date: string;\n}\n\nexport interface UsageBreakdown extends UsageTotals {\n  name: string;\n  provider?: string;\n}\n\nconst DAY_MS = 86_400_000;\n\nfunction dayStamp(value: string): number | null {\n  if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return null;\n  const stamp = Date.parse(`${value}T00:00:00.000Z`);\n  return Number.isFinite(stamp) && new Date(stamp).toISOString().slice(0, 10) === value\n    ? stamp\n    : null;\n}\n\nfunction isValidRecord(record: UsageRecord): boolean {\n  const values = [\n    record.inputTokens,\n    record.cachedInputTokens ?? 0,\n    record.outputTokens,\n    record.requests,\n    record.costUsd,\n  ];\n  return (\n    dayStamp(record.date) !== null &&\n    record.provider.trim().length > 0 &&\n    record.model.trim().length > 0 &&\n    (record.agent === undefined || record.agent.trim().length > 0) &&\n    values.every((value) => Number.isFinite(value) && value >= 0) &&\n    values.slice(0, 4).every(Number.isInteger) &&\n    (record.cachedInputTokens ?? 0) <= record.inputTokens\n  );\n}\n\nfunction emptyTotals(): UsageTotals {\n  return { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, requests: 0, costUsd: 0 };\n}\n\nfunction addTotals(target: UsageTotals, record: UsageRecord): void {\n  target.inputTokens += record.inputTokens;\n  target.cachedInputTokens += record.cachedInputTokens ?? 0;\n  target.outputTokens += record.outputTokens;\n  target.requests += record.requests;\n  target.costUsd += record.costUsd;\n}\n\n/** Aggregates a complete UTC window, including zero-use days. Never estimates cost. */\nexport function summarizeUsage(records: UsageRecord[], days: 7 | 30) {\n  const valid = records.filter(isValidRecord);\n  const latest = valid.reduce<number | null>((max, record) => {\n    const stamp = dayStamp(record.date);\n    return stamp === null ? max : max === null ? stamp : Math.max(max, stamp);\n  }, null);\n  if (latest === null) return null;\n\n  const first = latest - (days - 1) * DAY_MS;\n  const daily: DailyUsage[] = Array.from({ length: days }, (_, index) => ({\n    date: new Date(first + index * DAY_MS).toISOString().slice(0, 10),\n    ...emptyTotals(),\n  }));\n  const totals = emptyTotals();\n  const providers = new Map<string, UsageBreakdown>();\n  const agents = new Map<string, UsageBreakdown>();\n  const agentDaily = new Map<string, number[]>();\n  const models = new Map<string, UsageBreakdown>();\n  let recordCount = 0;\n\n  for (const record of valid) {\n    const stamp = dayStamp(record.date) ?? 0;\n    if (stamp < first || stamp > latest) continue;\n    recordCount += 1;\n    addTotals(daily[(stamp - first) / DAY_MS], record);\n    addTotals(totals, record);\n\n    let provider = providers.get(record.provider);\n    if (!provider) {\n      provider = { name: record.provider, ...emptyTotals() };\n      providers.set(record.provider, provider);\n    }\n    addTotals(provider, record);\n\n    const agentName = record.agent ?? record.provider;\n    const agentKey = `${agentName}\\0${record.provider}`;\n    let agent = agents.get(agentKey);\n    if (!agent) {\n      agent = { name: agentName, provider: record.provider, ...emptyTotals() };\n      agents.set(agentKey, agent);\n    }\n    addTotals(agent, record);\n    let dailyTokens = agentDaily.get(agentKey);\n    if (!dailyTokens) {\n      dailyTokens = Array(days).fill(0);\n      agentDaily.set(agentKey, dailyTokens);\n    }\n    dailyTokens[(stamp - first) / DAY_MS] += record.inputTokens + record.outputTokens;\n\n    const key = `${record.provider}\\0${record.model}`;\n    let model = models.get(key);\n    if (!model) {\n      model = { name: record.model, provider: record.provider, ...emptyTotals() };\n      models.set(key, model);\n    }\n    addTotals(model, record);\n  }\n\n  const byTokens = (a: UsageBreakdown, b: UsageBreakdown) =>\n    b.inputTokens + b.outputTokens - (a.inputTokens + a.outputTokens) || a.name.localeCompare(b.name);\n\n  const sortedAgents = [...agents.values()].sort(byTokens);\n  const agentNameCounts = new Map<string, number>();\n  for (const agent of sortedAgents) {\n    agentNameCounts.set(agent.name, (agentNameCounts.get(agent.name) ?? 0) + 1);\n  }\n\n  return {\n    daily,\n    totals,\n    providers: [...providers.values()].sort(byTokens),\n    agents: sortedAgents,\n    agentSeries: sortedAgents.map((agent) => ({\n      name: (agentNameCounts.get(agent.name) ?? 0) > 1 ? `${agent.name} · ${agent.provider}` : agent.name,\n      values: agentDaily.get(`${agent.name}\\0${agent.provider}`) ?? Array(days).fill(0),\n    })),\n    models: [...models.values()].sort(byTokens),\n    recordCount,\n    startDate: daily[0].date,\n    endDate: daily[daily.length - 1].date,\n  };\n}\n"},{"path":"components/agents/usage-provider-mark.tsx","type":"registry:component","target":"@components/agents/usage-provider-mark.tsx","content":"import { cn } from \"@/lib/utils\";\n\n/** Recognizable provider marks for the included coding-agent example. */\nexport function UsageProviderMark({\n  provider,\n  className,\n}: {\n  provider?: string;\n  className?: string;\n}) {\n  const name = provider?.trim().toLowerCase();\n  if (name !== \"openai\" && name !== \"anthropic\") return null;\n\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      aria-hidden=\"true\"\n      data-provider-mark={name}\n      className={cn(\"size-4 shrink-0\", className)}\n      fill=\"currentColor\"\n    >\n      <path d={name === \"openai\" ? OPENAI_MARK : CLAUDE_MARK} />\n    </svg>\n  );\n}\n\nconst OPENAI_MARK = \"M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z\";\nconst CLAUDE_MARK = \"m4.714 15.956 4.718-2.648.079-.23-.08-.128h-.23l-.79-.048-2.695-.073-2.337-.097-2.265-.122-.57-.121-.535-.704.055-.353.48-.321.685.06 1.518.104 2.277.157 1.651.098 2.447.255h.389l.054-.158-.133-.097-.103-.098-2.356-1.596-2.55-1.688-1.336-.972-.722-.491L2 6.223l-.158-1.008.655-.722.88.06.225.061.893.686 1.906 1.476 2.49 1.833.364.304.146-.104.018-.072-.164-.274-1.354-2.446-1.445-2.49-.644-1.032-.17-.619a2.972 2.972 0 0 1-.103-.729L6.287.133 6.7 0l.995.134.42.364.619 1.415L9.735 4.14l1.555 3.03.455.898.243.832.09.255h.159V9.01l.127-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.558 2.903-.365 1.942h.213l.243-.242.983-1.306 1.652-2.064.728-.82.85-.904.547-.431h1.032l.759 1.129-.34 1.166-1.063 1.347-.88 1.142-1.263 1.7-.79 1.36.074.11.188-.02 2.853-.606 1.542-.28 1.84-.315.832.388.09.395-.327.807-1.967.486-2.307.462-3.436.813-.043.03.049.061 1.548.146.662.036h1.62l3.018.225.79.522.473.638-.08.485-1.213.62-1.64-.389-3.825-.91-1.31-.329h-.183v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.321.455-.34-.049-2.204-1.657-.85-.747-1.925-1.62h-.127v.17l.443.649 2.343 3.521.122 1.08-.17.353-.607.213-.668-.122-1.372-1.924-1.415-2.168-1.141-1.943-.14.08-.674 7.254-.316.37-.728.28-.607-.461-.322-.747.322-1.476.388-1.924.316-1.53.285-1.9.17-.632-.012-.042-.14.018-1.432 1.967-2.18 2.945-1.724 1.845-.413.164-.716-.37.066-.662.401-.589 2.386-3.036 1.439-1.882.929-1.086-.006-.158h-.055L4.138 18.56l-1.13.146-.485-.456.06-.746.231-.243 1.907-1.312Z\";\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.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"}]}