{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-usage",
  "type": "registry:component",
  "title": "Context usage",
  "description": "A native disclosure of context occupancy, token categories and estimated costs from application-supplied rates or model catalogs.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/collapsible.json",
    "https://vlak.dev/r/progress.json",
    "https://vlak.dev/r/vlak-lib.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/context-usage.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { Collapsible } from \"./collapsible\";\nimport { Progress } from \"./progress\";\nimport { resolveContextPricing, type ContextPricingCatalog } from \"./context-pricing\";\n\nexport interface TokenUsage { inputTokens?: number; outputTokens?: number; reasoningTokens?: number; cachedInputTokens?: number }\nexport interface TokenPricing { inputPerMillion?: number; outputPerMillion?: number; reasoningPerMillion?: number; cacheReadPerMillion?: number; currency?: string }\nexport interface ContextUsageProps extends Omit<React.DetailsHTMLAttributes<HTMLDetailsElement>, \"title\"> {\n  usedTokens: number; maxTokens?: number; usage?: TokenUsage; pricing?: TokenPricing;\n  /** Optional metadata from an application-owned Tokenlens/models.dev catalog. Explicit limits and rates take precedence. */\n  modelId?: string; catalog?: ContextPricingCatalog;\n  model?: string; label?: string; defaultOpen?: boolean;\n}\nconst styles = stylex.create({\n  root: { width: \"100%\", minWidth: 0, color: vlak.ink },\n  body: { display: \"grid\", gap: \"0.75rem\" },\n  metadata: { margin: 0, fontSize: vlak.controlLabel, color: vlak.gray, lineHeight: 1.45 },\n  list: { margin: 0, display: \"grid\", gap: \"0.5rem\", fontSize: vlak.controlFs, fontVariantNumeric: \"tabular-nums\", lineHeight: 1.45 },\n  row: { display: \"flex\", flexWrap: \"wrap\", justifyContent: \"space-between\", gap: \"0.5rem 1rem\" },\n  value: { margin: 0, color: vlak.ink },\n});\nconst valid = (value: number | undefined): value is number => value !== undefined && Number.isFinite(value) && value >= 0;\nconst amount = (value: number | undefined) => valid(value) ? value.toLocaleString(\"en-US\") : \"Unavailable\";\n\n/** Context occupancy and caller-supplied token usage/pricing. No model catalog or prices are fetched. */\nexport const ContextUsage = React.forwardRef<HTMLDetailsElement, ContextUsageProps>(function ContextUsage({ usedTokens, maxTokens: explicitLimit, usage, pricing: explicitPricing, model: explicitModel, modelId, catalog, label = \"Context usage\", defaultOpen = false, className, style, ...props }, ref) {\n  const resolved = React.useMemo(() => modelId && catalog ? resolveContextPricing(modelId, catalog, { inputTokens: usage?.inputTokens }) : undefined, [modelId, catalog, usage?.inputTokens]);\n  const maxTokens = explicitLimit ?? resolved?.maxTokens;\n  const pricing = explicitPricing ?? resolved?.pricing;\n  const model = explicitModel ?? resolved?.model ?? modelId;\n  const known = valid(usedTokens) && valid(maxTokens) && maxTokens > 0;\n  const percentage = known ? Math.round(usedTokens / maxTokens * 100) : undefined;\n  const cached = usage?.cachedInputTokens;\n  const reasoning = usage?.reasoningTokens;\n  const input = usage?.inputTokens;\n  const output = usage?.outputTokens;\n  // Cached input is a subset of input; reasoning is a subset of output. Optional separate rates replace their parent rates.\n  const buckets = [\n    { count: input, rate: pricing?.inputPerMillion, subset: cached, subsetRate: pricing?.cacheReadPerMillion },\n    { count: output, rate: pricing?.outputPerMillion, subset: reasoning, subsetRate: pricing?.reasoningPerMillion },\n  ];\n  let cost: number | undefined;\n  if (pricing && buckets.every(bucket => valid(bucket.count) && (bucket.rate === undefined || valid(bucket.rate)) && (bucket.subsetRate === undefined || valid(bucket.subsetRate)) && (bucket.count === 0 || valid(bucket.rate)) && (bucket.subset === undefined || (valid(bucket.subset) && bucket.subset <= bucket.count)) && (bucket.count === 0 || bucket.subsetRate === undefined || bucket.subsetRate === bucket.rate || valid(bucket.subset)))) {\n    cost = buckets.reduce((sum, bucket) => {\n      const count = bucket.count!; const subset = valid(bucket.subset) && valid(bucket.subsetRate) ? bucket.subset : 0;\n      return sum + ((count - subset) * (bucket.rate ?? 0) + subset * (bucket.subsetRate ?? 0)) / 1_000_000;\n    }, 0);\n    if (!Number.isFinite(cost)) cost = undefined;\n  }\n  const money = cost === undefined ? \"Unavailable\" : `${cost.toLocaleString(\"en-US\", { minimumFractionDigits: 2, maximumFractionDigits: 6 })} ${pricing?.currency?.trim() || \"USD\"}`;\n  const root = rs([\"rs-context-usage\", className], styles.root);\n  const body = rs([\"rs-context-usage-body\"], styles.body);\n  const metadata = rs([\"rs-context-usage-metadata\"], styles.metadata);\n  const list = rs([\"rs-context-usage-list\"], styles.list);\n  const row = rs([\"rs-context-usage-row\"], styles.row);\n  const value = rs([\"rs-context-usage-value\"], styles.value);\n  const stats = [{ label: \"Input tokens\", value: amount(input) }, { label: \"Output tokens\", value: amount(output) }, { label: \"Reasoning tokens\", value: amount(reasoning) }, { label: \"Cached input tokens\", value: amount(cached) }];\n  return <Collapsible {...props} ref={ref} title={`${label}${percentage === undefined ? \"\" : ` · ${percentage}%`}`} defaultOpen={defaultOpen} className={root.className} style={{ ...root.style, ...style }}>\n    <div {...body}>{model && <p {...metadata}>{model}</p>}{known ? <Progress value={usedTokens} max={maxTokens} label={`${amount(usedTokens)} of ${amount(maxTokens)} tokens`} /> : <p {...metadata}>Context limit unavailable.</p>}\n      {known && usedTokens > maxTokens && <p {...metadata}>The supplied usage exceeds this context limit.</p>}\n      {usage && <dl {...list}>{stats.map(item => <div {...row} key={item.label}><dt>{item.label}</dt><dd {...value}>{item.value}</dd></div>)}</dl>}\n      {pricing && <div {...row}><span {...metadata}>Estimated cost from supplied rates</span><span {...value}>{money}</span></div>}\n    </div>\n  </Collapsible>;\n});\n",
      "type": "registry:component",
      "target": "components/vlak/context-usage.tsx"
    },
    {
      "path": "vlak/context-pricing.tsx",
      "content": "import type { TokenPricing } from \"./context-usage\";\n\n/** Tokenlens/models.dev catalog rates are USD per one million tokens. */\nexport interface ContextModelCost {\n  input?: number; output?: number; reasoning?: number; cache_read?: number;\n}\nexport interface ContextModelPricing {\n  id?: string; name?: string;\n  cost?: ContextModelCost & {\n    tiers?: readonly (ContextModelCost & { tier: { type?: \"context\"; size: number } })[];\n    context_over_200k?: ContextModelCost;\n  };\n  limit?: { context?: number; input?: number; output?: number };\n}\n/** Structurally accepts a supplied Tokenlens or models.dev provider catalog. */\nexport type ContextPricingCatalog = Readonly<Record<string, { id?: string; models: Readonly<Record<string, ContextModelPricing>> }>>;\nexport interface ResolvedContextPricing { modelId: string; model: string; maxTokens?: number; pricing?: TokenPricing }\nexport interface ContextPricingOptions { inputTokens?: number }\n\nconst valid = (value: number | undefined): value is number => typeof value === \"number\" && Number.isFinite(value) && value >= 0;\nconst normalizedId = (value: string) => value.trim().replace(/^([^/:]+):/, \"$1/\");\n\nfunction rates(model: ContextModelPricing, inputTokens: number | undefined): TokenPricing | undefined {\n  const cost = model.cost;\n  if (!cost) return undefined;\n  let selected: ContextModelCost = cost;\n  const tiers = cost.tiers?.length ? cost.tiers : cost.context_over_200k ? [{ ...cost.context_over_200k, tier: { size: 200_000 } }] : [];\n  if (tiers.length) {\n    // Choosing a tier requires the actual input count, not output or estimated context occupancy.\n    if (!valid(inputTokens) || tiers.some(entry => !valid(entry.tier.size) || (entry.tier.type !== undefined && entry.tier.type !== \"context\"))) return {};\n    const thresholds = new Set(tiers.map(entry => entry.tier.size));\n    if (thresholds.size !== tiers.length) return {};\n    const tier = [...tiers].sort((a, b) => b.tier.size - a.tier.size).find(entry => inputTokens > entry.tier.size);\n    if (tier) selected = tier;\n  }\n  if ([selected.input, selected.output, selected.reasoning, selected.cache_read].some(value => value !== undefined && !valid(value))) return {};\n  return {\n    inputPerMillion: valid(selected.input) ? selected.input : undefined,\n    outputPerMillion: valid(selected.output) ? selected.output : undefined,\n    reasoningPerMillion: valid(selected.reasoning) ? selected.reasoning : undefined,\n    cacheReadPerMillion: valid(selected.cache_read) ? selected.cache_read : undefined,\n    currency: \"USD\",\n  };\n}\n\n/**\n * Resolve cached catalog metadata without fetching, timers, or a bundled pricing snapshot.\n * Qualified provider/model ids select the provider's prices; providerless ids must be unique.\n * Catalogs may be obtained with Tokenlens or models.dev in application/server code.\n */\nexport function resolveContextPricing(modelId: string, catalog: ContextPricingCatalog, options: ContextPricingOptions = {}): ResolvedContextPricing | undefined {\n  const requested = normalizedId(modelId);\n  if (!requested) return undefined;\n  const matches: { modelId: string; model: ContextModelPricing }[] = [];\n  for (const [providerKey, provider] of Object.entries(catalog)) {\n    const providerIds = [providerKey, provider.id].filter((id): id is string => Boolean(id));\n    for (const [modelKey, model] of Object.entries(provider.models)) {\n      const localIds = [modelKey, model.id].filter((id): id is string => Boolean(id)).map(normalizedId);\n      const aliases = new Set(localIds.flatMap(id => providerIds.map(providerId => id.startsWith(`${providerId}/`) ? id : `${providerId}/${id}`)));\n      // A providerless lookup can be convenient, but cannot silently choose reseller pricing.\n      const providerless = !requested.includes(\"/\") && localIds.some(id => id === requested || providerIds.some(providerId => id === `${providerId}/${requested}`));\n      if (aliases.has(requested) || providerless) {\n        const providerId = provider.id || providerKey;\n        const localId = normalizedId(model.id || modelKey);\n        matches.push({ modelId: localId.startsWith(`${providerId}/`) ? localId : `${providerId}/${localId}`, model });\n      }\n    }\n  }\n  if (matches.length !== 1) return undefined;\n  const found = matches[0]!;\n  const limit = found.model.limit?.context;\n  return { modelId: found.modelId, model: found.model.name?.trim() || found.modelId, maxTokens: valid(limit) && limit > 0 ? limit : undefined, pricing: rates(found.model, options.inputTokens) };\n}\n",
      "type": "registry:component",
      "target": "components/vlak/context-pricing.tsx"
    },
    {
      "path": "vlak/styles/context-usage.css",
      "content": "/* ── context-usage: generated from packages/react/src/components/context-usage.tsx ── */\n.rs-context-usage{width:100%;min-width:0;color:var(--text)}\n.rs-context-usage-body{display:grid;gap:0.75rem}\n.rs-context-usage-metadata{margin:0;font-size:var(--control-label);color:var(--text-secondary);line-height:1.45}\n.rs-context-usage-list{margin:0;display:grid;gap:0.5rem;font-size:var(--control-fs);font-variant-numeric:tabular-nums;line-height:1.45}\n.rs-context-usage-row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:0.5rem 1rem}\n.rs-context-usage-value{margin:0;color:var(--text)}\n",
      "type": "registry:file",
      "target": "styles/vlak/context-usage.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "ai",
      "classes": [
        "rs-context-usage",
        "rs-context-usage-body",
        "rs-context-usage-metadata",
        "rs-context-usage-list",
        "rs-context-usage-row",
        "rs-context-usage-value"
      ],
      "snippet": "<details class=\"rs-context-usage rs-disclosure\"><summary class=\"rs-disclosure-summary\">Context usage · 25%</summary><div class=\"rs-context-usage-body\"><p class=\"rs-context-usage-metadata\">512 of 2,048 tokens</p></div></details>",
      "cssOnly": false,
      "registryDependencies": [
        "collapsible",
        "progress",
        "vlak-lib"
      ],
      "aliases": [
        "AI Elements Context",
        "Token usage",
        "Context window",
        "Token cost",
        "ContextUsage"
      ],
      "example": "import { ContextUsage, type ContextPricingCatalog } from \"@noorddev/vlak-react\";\n\nexport function ContextExample({ modelId, catalog }: {\n  modelId: string;\n  catalog: ContextPricingCatalog;\n}) {\n  return <ContextUsage\n    modelId={modelId}\n    catalog={catalog}\n    usedTokens={1500}\n    usage={{ inputTokens: 1000, outputTokens: 500, cachedInputTokens: 200, reasoningTokens: 100 }}\n  />;\n}",
      "usage": {
        "use": [
          "Supply usedTokens and maxTokens for context occupancy; usage supplies separate input, output, reasoning and cached-input counts.",
          "pricing accepts inputPerMillion, outputPerMillion, reasoningPerMillion, cacheReadPerMillion and optional currency. No model catalog or prices are fetched.",
          "Pass modelId and a supplied Tokenlens or models.dev catalog to resolve a model name, context limit, and rates. Explicit model, maxTokens, and pricing replace their resolved counterparts.",
          "resolveContextPricing exposes the same lookup for application composition. Qualified provider/model ids select provider pricing; a providerless id must match exactly one model.",
          "Catalog context tiers require the actual input token count through usage.inputTokens, or inputTokens in resolver options. Unknown, ambiguous, or invalid metadata remains unavailable.",
          "Cached input is a subset of input; reasoning is a subset of output. Separate rates replace the parent rate for those subsets rather than adding duplicate charges.",
          "Missing or invalid data displays Unavailable. Costs are estimates from the supplied rates, not provider billing totals.",
          "Use open/onToggle for a controlled native disclosure or defaultOpen for the initial state. CSS-only markup shows supplied values."
        ],
        "avoid": [
          "Treating missing token counts or unknown pricing as zero.",
          "Displaying rates without verifying them in the application that supplies them."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Focuses the native disclosure summary"
        },
        {
          "keys": "Enter, Space",
          "does": "Opens or closes usage details"
        }
      ],
      "a11y": [
        "The summary exposes current occupancy in text. The shared Progress names the token range and clamps its visual extent.",
        "Unknown limits omit the meter; usage above the supplied limit retains the real count and an explanation.",
        "Native details attributes and its ref are forwarded."
      ]
    }
  }
}
