{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context",
  "title": "Context",
  "author": "Jamie Thompson <jamie@jamiethompson.design> (https://jamiethompson.design)",
  "description": "Context-window usage indicator for AI chat: a ghost button showing percent used with a ring icon that opens a hover card with a progress bar, token counts (input, output, reasoning, cached) and a USD cost estimated by tokenlens from the model id, with reasoning billed as output and cached reads at the cache rate so the rows add up to the total. Use in a chat toolbar or prompt-input footer to show how much of the model's context a conversation has consumed.",
  "dependencies": [
    "ai@^7",
    "cn",
    "tokenlens@^1"
  ],
  "registryDependencies": [
    "button",
    "hover-card",
    "progress"
  ],
  "files": [
    {
      "path": "registry/ai/context.tsx",
      "content": "// Derived from Vercel AI Elements context.tsx (Apache-2.0, Copyright 2023 Vercel, Inc.).\n// Modified for uifiles: ported from Radix UI to Base UI; dependencies point at @uifiles.\n\"use client\"\n\nimport type { LanguageModelUsage } from \"ai\"\nimport { cn } from \"cn\"\nimport type { ComponentProps } from \"react\"\nimport { createContext, isValidElement, useContext, useMemo } from \"react\"\nimport { getUsage, type TokenBreakdown } from \"tokenlens\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@/components/ui/hover-card\"\nimport { Progress } from \"@/components/ui/progress\"\n\nconst PERCENT_MAX = 100\nconst ICON_RADIUS = 10\nconst ICON_VIEWBOX = 24\nconst ICON_CENTER = 12\nconst ICON_STROKE_WIDTH = 2\n\n// Number formatting is fixed to en-US (tokenlens prices in USD); see docs.\nconst LOCALE = \"en-US\"\nconst percentFormat = new Intl.NumberFormat(LOCALE, {\n  maximumFractionDigits: 1,\n  style: \"percent\",\n})\nconst compactFormat = new Intl.NumberFormat(LOCALE, { notation: \"compact\" })\nconst currencyFormat = new Intl.NumberFormat(LOCALE, {\n  currency: \"USD\",\n  style: \"currency\",\n})\n\ntype ModelId = string\n\ninterface ContextSchema {\n  usedTokens: number\n  maxTokens: number\n  usage?: LanguageModelUsage | undefined\n  modelId?: ModelId | undefined\n}\n\n/**\n * The usage split into non-overlapping rows: cached input reads and reasoning\n * output are subsets of `inputTokens` / `outputTokens` in ai@7, so they are\n * taken out of Input / Output and shown (and priced) on their own rows.\n */\ninterface UsageRows {\n  input: number\n  cache: number\n  output: number\n  reasoning: number\n}\n\n/** Each row's cost in USD, rounded to cents so the rows add up to `total`. */\ninterface UsageCosts extends UsageRows {\n  total: number\n}\n\ninterface ContextValue extends ContextSchema {\n  rows: UsageRows\n  costs: UsageCosts | undefined\n}\n\nconst ContextContext = createContext<ContextValue | null>(null)\n\nconst useContextValue = () => {\n  const context = useContext(ContextContext)\n\n  if (!context) {\n    throw new Error(\"Context components must be used within Context\")\n  }\n\n  return context\n}\n\n// Fraction of the window in use; 0 when the window is unknown (maxTokens\n// 0/undefined while model metadata loads) so no NaN% or ∞% is rendered.\nconst usedPercent = (used: number, max: number): number =>\n  Number.isFinite(used) && Number.isFinite(max) && max > 0\n    ? Math.max(0, used / max)\n    : 0\n\n// A missing, NaN or negative count (a JS consumer, usage still streaming)\n// reads as 0 everywhere, never as \"NaN\".\nconst count = (value: number | undefined): number =>\n  typeof value === \"number\" && Number.isFinite(value) && value > 0 ? value : 0\n\nconst formatTokens = (value: number | undefined): string =>\n  compactFormat.format(count(value))\n\nconst splitUsage = (usage: LanguageModelUsage | undefined): UsageRows => {\n  const cache = count(usage?.inputTokenDetails?.cacheReadTokens)\n  const reasoning = count(usage?.outputTokenDetails?.reasoningTokens)\n  return {\n    cache,\n    input: Math.max(0, count(usage?.inputTokens) - cache),\n    output: Math.max(0, count(usage?.outputTokens) - reasoning),\n    reasoning,\n  }\n}\n\nconst roundCents = (usd: number): number =>\n  Number.isFinite(usd) ? Math.round(usd * 100) / 100 : 0\n\nconst priceTokens = (modelId: ModelId, usage: TokenBreakdown): number =>\n  roundCents(getUsage({ modelId, usage }).costUSD?.totalUSD ?? 0)\n\n// Reasoning is billed at the output rate (tokenlens only prices\n// `reasoningTokens` for models with a separate reasoning rate, and the\n// bundled catalog has none); cache reads at the cache-read rate; the rest of\n// the input at the input rate. The total is the sum of the four rows.\nconst priceRows = (\n  modelId: ModelId | undefined,\n  rows: UsageRows\n): UsageCosts | undefined => {\n  if (!modelId) return undefined\n  const costs = {\n    cache: priceTokens(modelId, {\n      cacheReads: rows.cache,\n      input: 0,\n      output: 0,\n    }),\n    input: priceTokens(modelId, { input: rows.input, output: 0 }),\n    output: priceTokens(modelId, { input: 0, output: rows.output }),\n    reasoning: priceTokens(modelId, { input: 0, output: rows.reasoning }),\n  }\n  return {\n    ...costs,\n    total: roundCents(\n      costs.input + costs.cache + costs.output + costs.reasoning\n    ),\n  }\n}\n\nexport type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema\n\n// Base UI puts hover delays on the trigger, not the root: see ContextTrigger.\nexport const Context = ({\n  usedTokens,\n  maxTokens,\n  usage,\n  modelId,\n  ...props\n}: ContextProps) => {\n  const contextValue = useMemo<ContextValue>(() => {\n    const rows = splitUsage(usage)\n    return {\n      costs: priceRows(modelId, rows),\n      maxTokens,\n      modelId,\n      rows,\n      usage,\n      usedTokens,\n    }\n  }, [maxTokens, modelId, usage, usedTokens])\n\n  return (\n    <ContextContext.Provider value={contextValue}>\n      <HoverCard {...props} />\n    </ContextContext.Provider>\n  )\n}\n\nconst ContextIcon = () => {\n  const { usedTokens, maxTokens } = useContextValue()\n  const circumference = 2 * Math.PI * ICON_RADIUS\n  const percent = Math.min(1, usedPercent(usedTokens, maxTokens))\n  const dashOffset = circumference * (1 - percent)\n\n  return (\n    <svg\n      aria-label=\"Model context usage\"\n      height=\"20\"\n      role=\"img\"\n      style={{ color: \"currentcolor\" }}\n      viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}\n      width=\"20\"\n    >\n      <circle\n        cx={ICON_CENTER}\n        cy={ICON_CENTER}\n        fill=\"none\"\n        opacity=\"0.25\"\n        r={ICON_RADIUS}\n        stroke=\"currentColor\"\n        strokeWidth={ICON_STROKE_WIDTH}\n      />\n      <circle\n        cx={ICON_CENTER}\n        cy={ICON_CENTER}\n        fill=\"none\"\n        opacity=\"0.7\"\n        r={ICON_RADIUS}\n        stroke=\"currentColor\"\n        strokeDasharray={`${circumference} ${circumference}`}\n        strokeDashoffset={dashOffset}\n        strokeLinecap=\"round\"\n        strokeWidth={ICON_STROKE_WIDTH}\n        style={{ transform: \"rotate(-90deg)\", transformOrigin: \"center\" }}\n      />\n    </svg>\n  )\n}\n\nexport type ContextTriggerProps = ComponentProps<typeof Button> &\n  Pick<ComponentProps<typeof HoverCardTrigger>, \"delay\" | \"closeDelay\">\n\nexport const ContextTrigger = ({\n  children,\n  delay = 0,\n  closeDelay = 0,\n  ...props\n}: ContextTriggerProps) => {\n  const { usedTokens, maxTokens } = useContextValue()\n  const renderedPercent = percentFormat.format(\n    usedPercent(usedTokens, maxTokens)\n  )\n\n  // As upstream: a custom child element replaces the default button entirely.\n  return (\n    <HoverCardTrigger\n      closeDelay={closeDelay}\n      delay={delay}\n      render={\n        isValidElement(children) ? (\n          children\n        ) : (\n          <Button type=\"button\" variant=\"ghost\" {...props}>\n            {children ?? (\n              <>\n                <span className=\"font-medium text-muted-foreground\">\n                  {renderedPercent}\n                </span>\n                <ContextIcon />\n              </>\n            )}\n          </Button>\n        )\n      }\n    />\n  )\n}\n\nexport type ContextContentProps = ComponentProps<typeof HoverCardContent>\n\nexport const ContextContent = ({\n  className,\n  ...props\n}: ContextContentProps) => (\n  <HoverCardContent\n    className={cn(\"min-w-60 divide-y overflow-hidden p-0\", className)}\n    {...props}\n  />\n)\n\nexport type ContextContentHeaderProps = ComponentProps<\"div\">\n\nexport const ContextContentHeader = ({\n  children,\n  className,\n  ...props\n}: ContextContentHeaderProps) => {\n  const { usedTokens, maxTokens } = useContextValue()\n  const percent = usedPercent(usedTokens, maxTokens)\n  const displayPct = percentFormat.format(percent)\n  const used = formatTokens(usedTokens)\n  const total = formatTokens(maxTokens)\n\n  return (\n    <div className={cn(\"w-full space-y-2 p-3\", className)} {...props}>\n      {children ?? (\n        <>\n          <div className=\"flex items-center justify-between gap-3 text-xs\">\n            <p>{displayPct}</p>\n            <p className=\"font-mono text-muted-foreground\">\n              {used} / {total}\n            </p>\n          </div>\n          <div className=\"space-y-2\">\n            <Progress\n              aria-label=\"Context window usage\"\n              value={percent * PERCENT_MAX}\n            />\n          </div>\n        </>\n      )}\n    </div>\n  )\n}\n\nexport type ContextContentBodyProps = ComponentProps<\"div\">\n\nexport const ContextContentBody = ({\n  children,\n  className,\n  ...props\n}: ContextContentBodyProps) => (\n  <div className={cn(\"w-full p-3\", className)} {...props}>\n    {children}\n  </div>\n)\n\nexport type ContextContentFooterProps = ComponentProps<\"div\">\n\nexport const ContextContentFooter = ({\n  children,\n  className,\n  ...props\n}: ContextContentFooterProps) => {\n  const { costs } = useContextValue()\n  const totalCost = currencyFormat.format(costs?.total ?? 0)\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full items-center justify-between gap-3 bg-secondary p-3 text-xs\",\n        className\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <span className=\"text-secondary-foreground\">Total cost</span>\n          <span>{totalCost}</span>\n        </>\n      )}\n    </div>\n  )\n}\n\nconst TokensWithCost = ({\n  tokens,\n  costText,\n}: {\n  tokens?: number\n  costText?: string\n}) => (\n  <span>\n    {tokens === undefined ? \"—\" : formatTokens(tokens)}\n    {costText ? (\n      <span className=\"ml-2 text-muted-foreground\">• {costText}</span>\n    ) : null}\n  </span>\n)\n\nconst UsageRow = ({\n  label,\n  row,\n  className,\n  ...props\n}: ComponentProps<\"div\"> & { label: string; row: keyof UsageRows }) => {\n  const { rows, costs } = useContextValue()\n  const tokens = rows[row]\n\n  if (!tokens) {\n    return null\n  }\n\n  return (\n    <div\n      className={cn(\"flex items-center justify-between text-xs\", className)}\n      {...props}\n    >\n      <span className=\"text-muted-foreground\">{label}</span>\n      <TokensWithCost\n        costText={currencyFormat.format(costs?.[row] ?? 0)}\n        tokens={tokens}\n      />\n    </div>\n  )\n}\n\nexport type ContextInputUsageProps = ComponentProps<\"div\">\n\nexport const ContextInputUsage = ({\n  children,\n  ...props\n}: ContextInputUsageProps) =>\n  children ? children : <UsageRow label=\"Input\" row=\"input\" {...props} />\n\nexport type ContextOutputUsageProps = ComponentProps<\"div\">\n\nexport const ContextOutputUsage = ({\n  children,\n  ...props\n}: ContextOutputUsageProps) =>\n  children ? children : <UsageRow label=\"Output\" row=\"output\" {...props} />\n\nexport type ContextReasoningUsageProps = ComponentProps<\"div\">\n\nexport const ContextReasoningUsage = ({\n  children,\n  ...props\n}: ContextReasoningUsageProps) =>\n  children ? (\n    children\n  ) : (\n    <UsageRow label=\"Reasoning\" row=\"reasoning\" {...props} />\n  )\n\nexport type ContextCacheUsageProps = ComponentProps<\"div\">\n\nexport const ContextCacheUsage = ({\n  children,\n  ...props\n}: ContextCacheUsageProps) =>\n  children ? children : <UsageRow label=\"Cache\" row=\"cache\" {...props} />\n",
      "type": "registry:component",
      "target": "components/ai/context.tsx"
    }
  ],
  "docs": "Derived from Vercel AI Elements (Apache-2.0, Copyright 2023 Vercel, Inc.); ported to Base UI and maintained by uifiles. Built for the Base UI styles (base-nova): it composes the Base UI wrappers from components/ui and relies on their render prop and event signatures, so it does not work on Radix or React Aria styles. Base UI Preview Card puts hover delays on the trigger, not the root: Context no longer accepts openDelay/closeDelay; pass delay/closeDelay to ContextTrigger instead (both default 0). ContextTrigger composes HoverCardTrigger via render (a custom child element still replaces the default button, as upstream). Context's onOpenChange receives (open, eventDetails). ai@7 LanguageModelUsage moved fields: reasoning tokens are read from usage.outputTokenDetails.reasoningTokens and cached tokens from usage.inputTokenDetails.cacheReadTokens (upstream read usage.reasoningTokens / usage.cachedInputTokens). The Progress bar carries aria-label=\"Context window usage\" (upstream had no accessible name), and ContextContentFooter's label uses text-secondary-foreground for AA contrast on bg-secondary. Costs: the rows partition the usage so they add up to the footer. Input = inputTokens - cacheReadTokens at the input rate, Cache = cacheReadTokens at the cache-read rate, Output = outputTokens - reasoningTokens at the output rate, Reasoning = reasoningTokens at the output rate (tokenlens prices reasoning separately only for models with a reasoning rate; providers bill it as output). Each row is rounded to cents and Total cost is the sum of the rounded rows; a model tokenlens cannot price, or no modelId, shows $0.00. A row with zero tokens is omitted. usedTokens or maxTokens of 0, undefined, NaN, Infinity or a negative number (model metadata not loaded yet, or a JS consumer) renders as 0 in the header counts and as 0% in the trigger, progress bar and ring, never NaN or ∞; a usage field that is undefined, NaN or negative counts as 0 and its row is omitted. Numbers and currency use the fixed en-US locale (USD).",
  "categories": [
    "ai",
    "chat"
  ],
  "type": "registry:component"
}