{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cache-indicator",
  "title": "Cache Indicator",
  "description": "A prompt-cache chip for one request — hit / partial / miss / write derived from the token counts, a tooltip breakdown of cached vs fresh tokens and money saved, and a self-correcting TTL ring that flips the chip to expired and fires onExpire exactly once.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "tooltip",
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/cache-indicator.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { DatabaseBackup, DatabaseX, DatabaseZap, Layers, TimerOff } from \"lucide-react\"\n\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\n\n/* -------------------------------------------------------------------------- *\n * Outcome table\n *\n * One editable row per claim the chip is allowed to make about a prompt cache.\n * The ladder mirrors what a provider actually reports for a single request:\n * the prefix was read back (hit), partly read back (partial), not read at all\n * (miss), or paid for so the NEXT request can read it (write). `expired` is not\n * a request outcome — it is what the TTL says about the entry now.\n *\n * Colors are theme tokens, never literals: swap --chart-* and the whole ladder\n * re-skins, dark mode included.\n * -------------------------------------------------------------------------- */\n\nexport type CacheIndicatorState = \"hit\" | \"partial\" | \"miss\" | \"write\" | \"expired\"\n\ninterface StateMeta {\n  label: string\n  /** One sentence stating what this state actually claims — shown in the breakdown. */\n  description: string\n  color: string\n  glyph: (className: string) => React.ReactElement\n}\n\nconst STATES: Record<CacheIndicatorState, StateMeta> = {\n  hit: {\n    label: \"Cache hit\",\n    description: \"The whole prompt prefix was read back from the cache — nothing was re-processed.\",\n    color: \"var(--chart-2)\",\n    glyph: className => <DatabaseZap className={className} />,\n  },\n  partial: {\n    label: \"Partial cache\",\n    description: \"A prefix of the prompt matched the cache; everything after the break point was processed fresh.\",\n    color: \"var(--chart-4)\",\n    glyph: className => <Layers className={className} />,\n  },\n  miss: {\n    label: \"Cache miss\",\n    description: \"Nothing matched. The full prompt was processed at the uncached rate.\",\n    color: \"var(--muted-foreground)\",\n    glyph: className => <DatabaseX className={className} />,\n  },\n  write: {\n    label: \"Cache write\",\n    description: \"This request paid to write the prefix into the cache, so the next one can read it back.\",\n    color: \"var(--chart-1)\",\n    glyph: className => <DatabaseBackup className={className} />,\n  },\n  expired: {\n    label: \"Cache expired\",\n    description: \"The cached prefix has aged out of the window. The next request pays for the full prompt again.\",\n    color: \"var(--muted-foreground)\",\n    glyph: className => <TimerOff className={className} />,\n  },\n}\n\n/** Accent used while the window is about to close — urgency is a state, not a color literal. */\nconst URGENT_COLOR = \"var(--chart-5)\"\n\n/** Provider default for prompt caching: a five-minute sliding window. */\nconst DEFAULT_WINDOW = 5 * 60 * 1000\n\nconst SIZES = {\n  sm: { box: \"h-5 gap-1 px-1.5\", square: \"size-5\", glyph: \"size-3\", ring: \"size-3\", text: \"text-[0.6875rem]\" },\n  md: { box: \"h-6 gap-1.5 px-2\", square: \"size-6\", glyph: \"size-3.5\", ring: \"size-3.5\", text: \"text-xs\" },\n  lg: { box: \"h-7 gap-1.5 px-2.5\", square: \"size-7\", glyph: \"size-4\", ring: \"size-4\", text: \"text-sm\" },\n} as const\n\nexport type CacheIndicatorSize = keyof typeof SIZES\n\n/* -------------------------------------------------------------------------- *\n * Reduced motion\n * -------------------------------------------------------------------------- */\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n  mq.addEventListener(\"change\", callback)\n  return () => mq.removeEventListener(\"change\", callback)\n}\n\nfunction useReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  )\n}\n\n/* -------------------------------------------------------------------------- *\n * Formatting\n *\n * Every default formatter is deterministic on purpose. `Intl.NumberFormat` with\n * an implicit locale groups \"12,480\" on the server and \"12.480\" in a de-DE\n * browser, and this chip renders during hydration — so the defaults do their\n * own grouping and a consumer who wants locale rules passes their own function.\n * -------------------------------------------------------------------------- */\n\nfunction formatCount(value: number): string {\n  return Math.round(value)\n    .toString()\n    .replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\")\n}\n\nfunction formatDuration(ms: number): string {\n  const total = Math.max(0, Math.ceil(ms / 1000))\n  const hours = Math.floor(total / 3600)\n  const minutes = Math.floor((total % 3600) / 60)\n  const seconds = total % 60\n  if (hours > 0) return `${hours}h ${minutes.toString().padStart(2, \"0\")}m`\n  if (minutes > 0) return `${minutes}m ${seconds.toString().padStart(2, \"0\")}s`\n  return `${seconds}s`\n}\n\n/**\n * Coarse wording for the accessible name. The visible countdown ticks every\n * second, but an `aria-label` that changes every second makes a screen reader\n * re-announce the focused chip once a second — at minute granularity it changes\n * a handful of times per window.\n */\nfunction coarseDuration(ms: number): string {\n  const minutes = Math.floor(ms / 60000)\n  if (minutes >= 60) {\n    const hours = Math.round(minutes / 60)\n    return `about ${hours} hour${hours === 1 ? \"\" : \"s\"}`\n  }\n  if (minutes >= 1) return `about ${minutes} minute${minutes === 1 ? \"\" : \"s\"}`\n  return \"under a minute\"\n}\n\n/** Sub-cent savings must stay readable: $0.0037 never rounds down to $0.00. */\nfunction formatMoney(value: number, currency: string): string {\n  const abs = Math.abs(value)\n  return `${currency}${value.toFixed(abs > 0 && abs < 0.01 ? 4 : 2)}`\n}\n\n/**\n * Honest rounding. 99.6% cached must not print \"100%\" next to the word\n * \"partial\", and 0.4% cached must not print \"0%\" next to the word \"hit\" — both\n * ends are clamped instead of rounded.\n */\nfunction ratioPercent(cached: number, fresh: number): number | null {\n  const total = cached + fresh\n  if (total <= 0) return null\n  const raw = (cached / total) * 100\n  if (cached > 0 && raw < 1) return 1\n  if (fresh > 0 && raw > 99) return 99\n  return Math.round(raw)\n}\n\nfunction toCount(value: number | undefined): number {\n  return typeof value === \"number\" && Number.isFinite(value) && value > 0 ? Math.round(value) : 0\n}\n\nfunction toEpoch(value: Date | number | undefined): number | null {\n  if (value === undefined) return null\n  const ms = typeof value === \"number\" ? value : value.getTime()\n  return Number.isFinite(ms) ? ms : null\n}\n\n/* -------------------------------------------------------------------------- *\n * TTL ring\n *\n * Purely decorative: the exact remaining time is always available as text (in\n * the breakdown, and on the chip itself once the window turns urgent), so an\n * assistive technology never has to read a circle.\n * -------------------------------------------------------------------------- */\n\nconst RING_RADIUS = 9\nconst RING_LENGTH = 2 * Math.PI * RING_RADIUS\n\nfunction TtlRing({ className, fraction }: { className?: string; fraction: number }) {\n  const clamped = Math.min(1, Math.max(0, fraction))\n  return (\n    <svg aria-hidden=\"true\" className={cn(\"shrink-0 -rotate-90\", className)} viewBox=\"0 0 24 24\">\n      <circle className=\"stroke-current opacity-25\" cx=\"12\" cy=\"12\" fill=\"none\" r={RING_RADIUS} strokeWidth=\"3.5\" />\n      {/* The 1s linear transition matches the tick period exactly, so the arc\n          sweeps continuously instead of stepping once a second. */}\n      <circle\n        className=\"stroke-current transition-[stroke-dashoffset] duration-1000 ease-linear motion-reduce:transition-none\"\n        cx=\"12\"\n        cy=\"12\"\n        fill=\"none\"\n        r={RING_RADIUS}\n        strokeDasharray={RING_LENGTH}\n        strokeDashoffset={RING_LENGTH * (1 - clamped)}\n        strokeLinecap=\"round\"\n        strokeWidth=\"3.5\"\n      />\n    </svg>\n  )\n}\n\n/* -------------------------------------------------------------------------- *\n * Breakdown panel\n * -------------------------------------------------------------------------- */\n\nexport interface CacheIndicatorField {\n  label: string\n  value: React.ReactNode\n}\n\ninterface BreakdownProps {\n  cached: number\n  currency: string\n  description: React.ReactNode\n  expired: boolean\n  fields?: readonly CacheIndicatorField[]\n  formatRemaining: (ms: number) => string\n  formatTokens: (value: number) => string\n  fresh: number\n  glyph: React.ReactNode\n  label: React.ReactNode\n  percent: number | null\n  remaining: number | null\n  savings: number | null\n  written: number\n}\n\nfunction Breakdown({\n  cached,\n  currency,\n  description,\n  expired,\n  fields,\n  formatRemaining,\n  formatTokens,\n  fresh,\n  glyph,\n  label,\n  percent,\n  remaining,\n  savings,\n  written,\n}: BreakdownProps) {\n  const total = cached + fresh\n  const rows: CacheIndicatorField[] = []\n\n  if (total > 0) {\n    rows.push({\n      label: \"From cache\",\n      value: `${formatTokens(cached)} tokens${percent === null ? \"\" : ` · ${percent}%`}`,\n    })\n    rows.push({ label: \"Processed fresh\", value: `${formatTokens(fresh)} tokens` })\n  }\n  if (written > 0) rows.push({ label: \"Written to cache\", value: `${formatTokens(written)} tokens` })\n  if (savings !== null) rows.push({ label: \"Saved\", value: formatMoney(savings, currency) })\n  if (remaining !== null) {\n    rows.push({\n      label: expired ? \"Window\" : \"Expires\",\n      value: expired ? \"elapsed\" : `in ${formatRemaining(remaining)}`,\n    })\n  }\n  if (fields) rows.push(...fields)\n\n  return (\n    <div className=\"flex min-w-0 flex-col gap-2 text-left\">\n      {/* A div, not a <p>: `label` is consumer-supplied and may carry markup. */}\n      <div className=\"flex items-center gap-1.5 text-xs font-medium\">\n        <span aria-hidden=\"true\" className=\"inline-flex shrink-0 items-center\">\n          {glyph}\n        </span>\n        {label}\n      </div>\n\n      {description ? <p className=\"text-xs leading-snug text-pretty opacity-70\">{description}</p> : null}\n\n      {/* Cached vs fresh as one bar. A tooltip paints on an inverted surface, so\n          every colour in here is currentColor — an accent would not survive it. */}\n      {total > 0 ? (\n        <div aria-hidden=\"true\" className=\"relative h-1 w-full overflow-hidden rounded-full\">\n          <div className=\"absolute inset-0 bg-current opacity-25\" />\n          <div className=\"absolute inset-y-0 left-0 bg-current\" style={{ width: `${(cached / total) * 100}%` }} />\n        </div>\n      ) : null}\n\n      {rows.length > 0 ? (\n        <dl className=\"grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-0.5 text-xs\">\n          {rows.map((row, i) => (\n            <React.Fragment key={`${row.label}-${i}`}>\n              <dt className=\"font-normal opacity-70\">{row.label}</dt>\n              <dd className=\"min-w-0 font-medium tabular-nums wrap-anywhere\">{row.value}</dd>\n            </React.Fragment>\n          ))}\n        </dl>\n      ) : null}\n    </div>\n  )\n}\n\n/* -------------------------------------------------------------------------- *\n * Component\n * -------------------------------------------------------------------------- */\n\nexport interface CacheIndicatorPricing {\n  /** Price of 1M uncached input tokens, in whatever currency you pass. */\n  fresh: number\n  /** Price of 1M cache-read tokens (default 0 — every cached token counted as free). */\n  cached?: number\n}\n\nexport interface CacheIndicatorProps extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n  /** What this request did with the cache. Omit to derive it from the token counts. */\n  state?: CacheIndicatorState\n  /** Prompt tokens read back from the cache (`cache_read_input_tokens`). */\n  cachedTokens?: number\n  /** Prompt tokens the model had to process (uncached input). */\n  freshTokens?: number\n  /** Prompt tokens this request wrote into the cache (`cache_creation_input_tokens`). */\n  writtenTokens?: number\n  /** Absolute expiry of the cached prefix. Preferred: it survives re-renders and prop churn. */\n  expiresAt?: Date | number\n  /**\n   * Length of the cache window in ms — the ring's denominator (default 300000).\n   * Passed WITHOUT `expiresAt` it also anchors the window at mount, which is\n   * what a live badge wants; changing it re-anchors.\n   */\n  ttlMs?: number\n  /** Remaining time below which the window turns urgent. Default `min(60s, window / 4)`. */\n  warnAt?: number\n  /**\n   * How the remaining time is shown. `auto` = ring, plus the numeric value once\n   * urgent, and numeric-only under reduced motion.\n   */\n  countdown?: \"auto\" | \"ring\" | \"text\" | \"none\"\n  /** Fired once, when the window elapses while this chip is mounted. Never fires twice. */\n  onExpire?: () => void\n  /** Per-million-token prices; enables the \"Saved\" row in the breakdown. */\n  pricing?: CacheIndicatorPricing\n  /** Money prefix, e.g. \"$\" or \"€\". A plain string, so no locale can shift it between server and client. */\n  currency?: string\n  /** Override the chip text. The default comes from the resolved state. */\n  label?: React.ReactNode\n  /** Sentence at the top of the breakdown; pass `null` to drop it. */\n  description?: React.ReactNode\n  /** Append the cached share (e.g. \"96%\") to the chip label. Hit and partial only. */\n  showRatio?: boolean\n  /** false shrinks the chip to a square glyph; the accessible name is kept. */\n  showLabel?: boolean\n  size?: CacheIndicatorSize\n  /** soft = tinted capsule, outline = hairline capsule. */\n  tone?: \"soft\" | \"outline\"\n  /** Accent override — pass a theme token such as \"var(--primary)\", never a literal color. Also opts out of the urgency tint. */\n  color?: string\n  /** `undefined` uses the state glyph; `null` / `false` renders none; any node replaces it. */\n  icon?: React.ReactNode\n  /** Disclosure surface. Defaults to `tooltip` when there are numbers to reveal, `none` otherwise. */\n  details?: \"none\" | \"tooltip\"\n  /** Extra breakdown rows (cache key, break point, provider…), rendered after the built-in ones. */\n  fields?: readonly CacheIndicatorField[]\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n  align?: \"start\" | \"center\" | \"end\"\n  /** Tooltip open delay in ms. */\n  delayDuration?: number\n  formatRemaining?: (ms: number) => string\n  formatTokens?: (value: number) => string\n}\n\nexport const CacheIndicator = React.forwardRef<HTMLElement, CacheIndicatorProps>(function CacheIndicator(\n  {\n    state,\n    cachedTokens,\n    freshTokens,\n    writtenTokens,\n    expiresAt,\n    ttlMs,\n    warnAt,\n    countdown = \"auto\",\n    onExpire,\n    pricing,\n    currency = \"$\",\n    label,\n    description,\n    showRatio = true,\n    showLabel = true,\n    size = \"md\",\n    tone = \"soft\",\n    color,\n    icon,\n    details,\n    fields,\n    side = \"top\",\n    align = \"center\",\n    delayDuration = 200,\n    formatRemaining = formatDuration,\n    formatTokens = formatCount,\n    className,\n    style,\n    ...props\n  },\n  ref,\n) {\n  const reduced = useReducedMotion()\n\n  const cached = toCount(cachedTokens)\n  const fresh = toCount(freshTokens)\n  const written = toCount(writtenTokens)\n  const totalPrompt = cached + fresh\n\n  const expiresAtMs = toEpoch(expiresAt)\n  const hasTtl = typeof ttlMs === \"number\" && Number.isFinite(ttlMs) && ttlMs > 0\n  const windowMs = hasTtl ? (ttlMs as number) : DEFAULT_WINDOW\n  // A relative window is only anchored when the caller actually asked for one.\n  const anchorAtMount = expiresAtMs === null && hasTtl\n\n  // Keeping the callback in a ref means a fresh inline closure on every parent\n  // render cannot restart the timer (and reset the one-shot lock with it).\n  const onExpireRef = React.useRef(onExpire)\n  React.useEffect(() => {\n    onExpireRef.current = onExpire\n  })\n\n  /* The countdown is state that only the timer writes. Resolving a deadline\n     needs `Date.now()`, and reading the wall clock during render gives the\n     server one answer and the client another — a hydration mismatch on every\n     badge on the page. Until the first tick lands there is simply no countdown.\n     `expiresAtMs` is a number, so a caller re-creating the same Date on every\n     render does not restart anything. */\n  const [remaining, setRemaining] = React.useState<number | null>(null)\n\n  React.useEffect(() => {\n    /* Two locks, both scoped to this window: `sawLive` means it was still open\n       at some point while this chip was mounted, `fired` means the callback has\n       already run. Mounting on an already-dead cache is not an expiry event —\n       it is history — so the callback stays silent. */\n    let sawLive = false\n    let fired = false\n    let armed = false\n    let deadline = 0\n    let timer: ReturnType<typeof setTimeout> | undefined\n\n    const tick = () => {\n      const left = Math.max(0, deadline - Date.now())\n      setRemaining(left)\n\n      if (left > 0) {\n        sawLive = true\n        /* Self-correcting schedule: sleep exactly until the displayed second\n           changes, recomputed from the wall clock every time. A fixed 1000ms\n           interval drifts, and a throttled background tab makes it skip\n           numbers on return. */\n        timer = setTimeout(tick, Math.max(50, ((left - 1) % 1000) + 1))\n        return\n      }\n\n      // Elapsed: stop scheduling entirely — an expired badge costs nothing.\n      if (sawLive && !fired) {\n        fired = true\n        onExpireRef.current?.()\n      }\n    }\n\n    const start = () => {\n      if (expiresAtMs === null && !anchorAtMount) {\n        setRemaining(null)\n        return\n      }\n      deadline = expiresAtMs ?? Date.now() + windowMs\n      armed = true\n      tick()\n    }\n\n    // Deferred by one task on purpose: no state is written from the effect body\n    // itself, so arming a countdown can never cascade a synchronous re-render.\n    timer = setTimeout(start, 0)\n\n    // A hidden tab throttles timers to about once a minute; resync the moment\n    // it comes back so nobody tabs in to a stale number.\n    const onVisible = () => {\n      if (document.visibilityState !== \"visible\" || !armed) return\n      clearTimeout(timer)\n      tick()\n    }\n    document.addEventListener(\"visibilitychange\", onVisible)\n\n    return () => {\n      clearTimeout(timer)\n      document.removeEventListener(\"visibilitychange\", onVisible)\n    }\n  }, [expiresAtMs, anchorAtMount, windowMs])\n\n  const elapsed = remaining !== null && remaining <= 0\n\n  /* Outcome vs window. The token counts say what THIS request did; the TTL says\n     whether the entry is still worth anything to the NEXT one — and once the\n     window is gone that is the more useful headline, so it supersedes. */\n  const derived: CacheIndicatorState =\n    cached > 0 && fresh > 0 ? \"partial\" : cached > 0 ? \"hit\" : written > 0 ? \"write\" : \"miss\"\n  const resolved: CacheIndicatorState = elapsed ? \"expired\" : (state ?? derived)\n  const expired = resolved === \"expired\"\n\n  const warnMs = typeof warnAt === \"number\" && Number.isFinite(warnAt) ? warnAt : Math.min(60000, windowMs / 4)\n  const urgent = remaining !== null && remaining > 0 && remaining <= warnMs\n\n  const meta = STATES[resolved]\n  const accent = color ?? (urgent && !expired ? URGENT_COLOR : meta.color)\n  const sizes = SIZES[size]\n  const text = label ?? meta.label\n  const stateName = typeof text === \"string\" ? text : meta.label\n  const blurb = description === undefined ? meta.description : description\n\n  const glyph = icon === undefined ? meta.glyph(sizes.glyph) : icon\n  // An icon-only chip with no icon would be an empty capsule: keep the text.\n  const labelVisible = showLabel || !glyph\n\n  const percent = ratioPercent(cached, fresh)\n  const ratioText =\n    showRatio && percent !== null && (resolved === \"hit\" || resolved === \"partial\") ? `${percent}%` : null\n\n  const rawSavings = pricing && cached > 0 ? (cached / 1e6) * (pricing.fresh - (pricing.cached ?? 0)) : null\n  const savings = rawSavings !== null && Number.isFinite(rawSavings) && rawSavings > 0 ? rawSavings : null\n\n  // Countdown presentation. Reduced motion drops the sweeping arc and keeps the\n  // number — the information survives, only the animation goes.\n  const liveMs = remaining !== null && remaining > 0 ? remaining : null\n  const showCountdown = liveMs !== null && countdown !== \"none\"\n  const countdownAsText = showCountdown && (countdown === \"text\" || (countdown === \"auto\" && (reduced || urgent)))\n  const countdownAsRing = showCountdown && countdown !== \"text\" && !(countdown === \"auto\" && reduced)\n\n  /* The accessible sentence. The remaining time is stated at MINUTE\n     granularity: a name that changes every second makes a screen reader\n     re-announce the focused chip once a second. */\n  const detail = [\n    totalPrompt > 0 ? `${formatCount(cached)} of ${formatCount(totalPrompt)} prompt tokens from cache` : null,\n    written > 0 ? `${formatCount(written)} tokens written to cache` : null,\n    remaining === null ? null : remaining > 0 ? `expires in ${coarseDuration(remaining)}` : \"cache window elapsed\",\n  ]\n    .filter((part): part is string => part !== null)\n    .join(\", \")\n  const summary = detail ? `${stateName}, ${detail}` : stateName\n\n  /* Derived from the PROPS, never from the clock: a chip that promoted itself\n     from a <span> to a <button> once the first tick landed would swap DOM nodes\n     under the pointer and drop focus. */\n  const hasBreakdown =\n    totalPrompt > 0 || written > 0 || expiresAtMs !== null || anchorAtMount || (fields?.length ?? 0) > 0\n  // Affordance follows content: a chip with no numbers behind it is not a focus\n  // stop and gets no pointer cursor.\n  const mode = details ?? (hasBreakdown ? \"tooltip\" : \"none\")\n  const interactive = mode === \"tooltip\"\n\n  const surface: React.CSSProperties =\n    tone === \"soft\"\n      ? { backgroundColor: `color-mix(in oklab, ${accent} 14%, transparent)`, color: accent }\n      : { borderColor: `color-mix(in oklab, ${accent} 45%, transparent)`, color: accent }\n\n  const content = (\n    <>\n      {glyph ? (\n        <span aria-hidden=\"true\" className=\"inline-flex shrink-0 items-center\">\n          {glyph}\n        </span>\n      ) : null}\n\n      {labelVisible ? <span className=\"min-w-0 truncate\">{text}</span> : null}\n      {labelVisible && ratioText ? <span className=\"shrink-0 tabular-nums opacity-80\">{ratioText}</span> : null}\n\n      {countdownAsRing && liveMs !== null ? <TtlRing className={sizes.ring} fraction={liveMs / windowMs} /> : null}\n      {countdownAsText && liveMs !== null ? (\n        <span className=\"shrink-0 tabular-nums opacity-80\">{formatRemaining(liveMs)}</span>\n      ) : null}\n\n      {/* The visible label alone under-reports; the counts live in a tooltip a\n          screen reader will not open. An interactive chip states the whole\n          sentence in its aria-label instead, so this node only exists for the\n          inert one — and it drops the part the visible label already says. */}\n      {!interactive && (labelVisible ? detail : summary) ? (\n        <span className=\"sr-only\">{labelVisible ? detail : summary}</span>\n      ) : null}\n    </>\n  )\n\n  const shared = {\n    className: cn(\n      // No select-none: a cache annotation should travel with the text when a\n      // reader copies the line it sits in.\n      \"relative inline-flex w-fit shrink-0 items-center justify-center rounded-full align-middle font-medium whitespace-nowrap\",\n      sizes.text,\n      labelVisible ? sizes.box : sizes.square,\n      tone === \"outline\" && \"border\",\n      interactive &&\n        cn(\n          \"cursor-pointer transition-shadow duration-150 select-none motion-reduce:transition-none\",\n          \"hover:ring-2 hover:ring-ring/30 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\",\n          \"data-[state=delayed-open]:ring-2 data-[state=delayed-open]:ring-ring/40\",\n          \"data-[state=instant-open]:ring-2 data-[state=instant-open]:ring-ring/40\",\n        ),\n      className,\n    ),\n    // Not `data-state`: Radix owns that attribute on a tooltip trigger.\n    \"data-cache\": resolved,\n    \"data-expiring\": urgent ? \"\" : undefined,\n    \"data-size\": size,\n    style: { ...surface, ...style },\n    ...props,\n  }\n\n  if (!interactive) {\n    return (\n      <span ref={ref as React.Ref<HTMLSpanElement>} {...shared}>\n        {content}\n      </span>\n    )\n  }\n\n  return (\n    <TooltipProvider delayDuration={delayDuration}>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <button aria-label={summary} ref={ref as React.Ref<HTMLButtonElement>} type=\"button\" {...shared}>\n            {content}\n          </button>\n        </TooltipTrigger>\n        <TooltipContent align={align} className=\"max-w-72 items-start\" side={side} sideOffset={6}>\n          <Breakdown\n            cached={cached}\n            currency={currency}\n            description={blurb}\n            expired={expired}\n            fields={fields}\n            formatRemaining={formatRemaining}\n            formatTokens={formatTokens}\n            fresh={fresh}\n            glyph={meta.glyph(\"size-3.5\")}\n            label={text}\n            percent={percent}\n            remaining={remaining}\n            savings={savings}\n            written={written}\n          />\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  )\n})\n\nCacheIndicator.displayName = \"CacheIndicator\"\n\nexport default CacheIndicator\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}