{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-meter",
  "title": "Context Meter",
  "description": "How full a model's context window is: a composer ring, a stacked breakdown, threshold warnings, and compaction that shows what it freed.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "popover",
    "https://uaiblocks.dev/r/uai-number-tick.json",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/context-meter.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { Check, FoldVertical, LoaderCircle, Undo2 } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { NumberTick } from \"@/lib/uai-number-tick\";\nimport { cn } from \"@/lib/uai-utils\";\n\n/** `bar` first: it is the full card. `ring` sits in a composer and `compact` in sidebars and headers. */\nexport const CONTEXT_METER_VARIANTS = [\"bar\", \"ring\", \"compact\"] as const;\nexport const CONTEXT_METER_STATUSES = [\"idle\", \"compacting\", \"compacted\"] as const;\n\nexport type ContextMeterVariant = (typeof CONTEXT_METER_VARIANTS)[number];\nexport type ContextMeterStatus = (typeof CONTEXT_METER_STATUSES)[number];\nexport type ContextMeterLevel = \"empty\" | \"normal\" | \"warning\" | \"full\";\nexport type ContextMeterSegment = { id: string; label: string; tokens: number };\n\n/** Compact English token counts: 980, 76.4k, 200k, 1.2M. */\nexport function formatTokenCount(tokens: number) {\n  const trim = (value: number) => value.toFixed(1).replace(/\\.0$/, \"\");\n  if (Math.abs(tokens) >= 1_000_000) return `${trim(tokens / 1_000_000)}M`;\n  if (Math.abs(tokens) >= 1_000) return `${trim(tokens / 1_000)}k`;\n  return String(Math.round(tokens));\n}\n\nconst fullNumber = new Intl.NumberFormat(\"en-US\");\n\nexport type ContextMeterProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  variant?: ContextMeterVariant;\n  /** Tokens in the context window now. */\n  used: number;\n  /** The model's context window. */\n  limit: number;\n  /** What the used tokens are made of, in order. Their sum should equal `used`. */\n  segments?: readonly ContextMeterSegment[];\n  /** Fraction at which the meter turns warning. */\n  warnAt?: number;\n  /** Fraction at which the meter turns full. */\n  fullAt?: number;\n  /** Fraction at which the consumer summarizes older turns automatically, for the hint. */\n  autoCompactAt?: number;\n  status?: ContextMeterStatus;\n  /** Tokens the last compaction freed, drawn as a hatched region after the used tokens. */\n  freed?: number;\n  /** The segment that compaction summarizes. Defaults to the last segment. */\n  compactSegment?: string;\n  formatTokens?: (tokens: number) => string;\n  onCompact?: () => void;\n  onUndoCompact?: () => void;\n  children?: ReactNode;\n};\n\ntype ContextMeterContextValue = {\n  variant: ContextMeterVariant;\n  used: number;\n  limit: number;\n  percent: number;\n  level: ContextMeterLevel;\n  segments: readonly ContextMeterSegment[];\n  warnAt: number;\n  fullAt: number;\n  autoCompactAt: number | undefined;\n  status: ContextMeterStatus;\n  freed: number | undefined;\n  compactSegment: string | undefined;\n  format: (tokens: number) => string;\n  valueText: string;\n  pulse: number;\n  onCompact: (() => void) | undefined;\n  onUndoCompact: (() => void) | undefined;\n};\n\nconst ContextMeterContext = createContext<ContextMeterContextValue | null>(null);\n\nfunction useContextMeter(name: string) {\n  const context = useContext(ContextMeterContext);\n  if (!context) throw new Error(`${name} must be used within ContextMeter`);\n  return context;\n}\n\nconst LEVEL_ORDER: Record<ContextMeterLevel, number> = { empty: 0, normal: 0, warning: 1, full: 2 };\n\nexport function ContextMeter({\n  variant = \"bar\",\n  used,\n  limit,\n  segments = [],\n  warnAt = 0.8,\n  fullAt = 0.95,\n  autoCompactAt,\n  status = \"idle\",\n  freed,\n  compactSegment,\n  formatTokens = formatTokenCount,\n  onCompact,\n  onUndoCompact,\n  children,\n  className,\n  ...props\n}: ContextMeterProps) {\n  const ratio = limit > 0 ? Math.min(1, Math.max(0, used / limit)) : 0;\n  const percent = Math.round(ratio * 100);\n  const level: ContextMeterLevel =\n    used <= 0 ? \"empty\" : ratio >= fullAt ? \"full\" : ratio >= warnAt ? \"warning\" : \"normal\";\n  const valueText = `${fullNumber.format(used)} of ${fullNumber.format(limit)} tokens, ${percent} percent`;\n\n  // Announce once per upward crossing, never on the way down or on every render.\n  const previousLevel = useRef(level);\n  const previousStatus = useRef(status);\n  const [announcement, setAnnouncement] = useState(\"\");\n  const [pulse, setPulse] = useState(0);\n  useEffect(() => {\n    const before = previousLevel.current;\n    previousLevel.current = level;\n    if (LEVEL_ORDER[level] > LEVEL_ORDER[before]) {\n      const hint =\n        autoCompactAt !== undefined\n          ? ` Older turns will be summarized at ${Math.round(autoCompactAt * 100)}%.`\n          : \"\";\n      setAnnouncement(`Context ${percent}% full.${hint}`);\n      setPulse((count) => count + 1);\n    }\n  }, [level, percent, autoCompactAt]);\n  useEffect(() => {\n    const before = previousStatus.current;\n    previousStatus.current = status;\n    if (before === status) return;\n    if (status === \"compacting\") setAnnouncement(\"Compacting conversation\");\n    if (status === \"compacted\")\n      setAnnouncement(\n        freed === undefined ? \"Compacted.\" : `Compacted. Freed ${fullNumber.format(freed)} tokens.`,\n      );\n  }, [status, freed]);\n\n  const value: ContextMeterContextValue = {\n    variant,\n    used,\n    limit,\n    percent,\n    level,\n    segments,\n    warnAt,\n    fullAt,\n    autoCompactAt,\n    status,\n    freed,\n    compactSegment: compactSegment ?? segments.at(-1)?.id,\n    format: formatTokens,\n    valueText,\n    pulse,\n    onCompact,\n    onUndoCompact,\n  };\n\n  const live = (\n    <span role=\"status\" className=\"sr-only\">\n      {announcement}\n    </span>\n  );\n\n  if (variant === \"bar\") {\n    return (\n      <ContextMeterContext.Provider value={value}>\n        <section\n          data-slot=\"context-meter\"\n          data-variant={variant}\n          data-level={level}\n          data-status={status}\n          className={cn(\n            \"grid w-full min-w-0 gap-3.5 rounded-[14px] border border-border bg-card p-4 text-[13px]/[18px] text-card-foreground\",\n            className,\n          )}\n          {...(props as ComponentProps<\"section\">)}\n        >\n          {children}\n          {live}\n        </section>\n      </ContextMeterContext.Provider>\n    );\n  }\n\n  return (\n    <ContextMeterContext.Provider value={value}>\n      <Popover>\n        <div\n          data-slot=\"context-meter\"\n          data-variant={variant}\n          data-level={level}\n          data-status={status}\n          className={cn(\"relative inline-flex shrink-0 items-center\", className)}\n          {...props}\n        >\n          {/* One meter per instance: the trigger carries the name, this carries the value. */}\n          {/* biome-ignore lint/a11y/useSemanticElements: matches the bar's styled meter so both variants read alike. */}\n          <span\n            role=\"meter\"\n            aria-label=\"Context window\"\n            aria-valuemin={0}\n            aria-valuemax={limit}\n            aria-valuenow={Math.min(used, limit)}\n            aria-valuetext={valueText}\n            className=\"sr-only\"\n          />\n          {children}\n          {live}\n        </div>\n      </Popover>\n    </ContextMeterContext.Provider>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Ring and trigger\n * -----------------------------------------------------------------------------------------------*/\n\nexport type ContextMeterRingProps = Omit<ComponentProps<\"svg\">, \"children\">;\n\n/** An 18px ring (14px in compact): the value against a strong-border track, colored only by state. */\nexport function ContextMeterRing({ className, ...props }: ContextMeterRingProps) {\n  const context = useContextMeter(\"ContextMeterRing\");\n  const value = context.level === \"empty\" ? 0 : context.percent;\n  return (\n    <svg\n      data-slot=\"context-meter-ring\"\n      viewBox=\"0 0 20 20\"\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      className={cn(\n        \"-rotate-90 fill-none\",\n        context.variant === \"compact\" ? \"size-3.5\" : \"size-[18px]\",\n        className,\n      )}\n      {...props}\n    >\n      <circle cx=\"10\" cy=\"10\" r=\"7.5\" strokeWidth=\"2.5\" className=\"stroke-border-strong\" />\n      {value > 0 ? (\n        <circle\n          cx=\"10\"\n          cy=\"10\"\n          r=\"7.5\"\n          strokeWidth=\"2.5\"\n          strokeLinecap=\"round\"\n          pathLength={100}\n          strokeDasharray={`${value} 100`}\n          className={cn(\n            \"[transition:stroke-dasharray_300ms_cubic-bezier(0.23,1,0.32,1),stroke_240ms_ease-out] motion-reduce:transition-none\",\n            context.level === \"full\"\n              ? \"stroke-destructive\"\n              : context.level === \"warning\"\n                ? \"stroke-warning\"\n                : \"stroke-foreground\",\n          )}\n        />\n      ) : null}\n    </svg>\n  );\n}\n\nconst triggerVariants = cva(\n  \"inline-flex shrink-0 cursor-pointer items-center border-0 font-medium tabular-nums [transition:background-color_120ms_ease-out,color_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100\",\n  {\n    variants: {\n      variant: {\n        bar: \"\",\n        ring: \"h-7 gap-1.5 rounded-lg bg-transparent py-0 pr-2 pl-1.5 text-[11.5px] text-muted-foreground hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground has-[>svg]:pr-2 has-[>svg]:pl-1.5\",\n        compact:\n          \"h-6 gap-1.5 rounded-full bg-muted py-0 pr-2.5 pl-1.5 text-[11.5px] text-muted-foreground hover:bg-muted hover:text-foreground has-[>svg]:pr-2.5 has-[>svg]:pl-1.5\",\n      },\n      level: {\n        empty: \"\",\n        normal: \"\",\n        warning: \"\",\n        full: \"\",\n      },\n    },\n    compoundVariants: [\n      {\n        variant: \"compact\",\n        level: \"warning\",\n        className: \"bg-warning/14 text-warning-ink hover:bg-warning/14 hover:text-warning-ink\",\n      },\n      {\n        variant: \"compact\",\n        level: \"full\",\n        className:\n          \"bg-destructive/14 text-destructive-ink hover:bg-destructive/14 hover:text-destructive-ink\",\n      },\n    ],\n  },\n);\n\nexport type ContextMeterTriggerProps = ComponentProps<\"button\">;\n\n/**\n * Ring and compact: a 28px ghost button (a 24px tinted pill in compact) with the ring and the\n * percent. It opens the breakdown in a Popover. Renders nothing in the bar variant.\n */\nexport function ContextMeterTrigger({ className, children, ...props }: ContextMeterTriggerProps) {\n  const context = useContextMeter(\"ContextMeterTrigger\");\n  if (context.variant === \"bar\") return null;\n  const label = `Context: ${context.percent}% used`;\n  return (\n    <PopoverTrigger asChild>\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        data-slot=\"context-meter-trigger\"\n        aria-label={label}\n        className={cn(\n          triggerVariants({ variant: context.variant, level: context.level }),\n          context.level === \"warning\" && \"text-warning-ink\",\n          context.level === \"full\" && \"text-destructive-ink\",\n          className,\n        )}\n        {...props}\n      >\n        <span\n          key={context.pulse}\n          aria-hidden=\"true\"\n          className={cn(\n            \"grid place-items-center rounded-full\",\n            context.pulse > 0 &&\n              \"motion-safe:animate-ring-pulse motion-safe:[animation-iteration-count:1]\",\n          )}\n        >\n          <ContextMeterRing />\n        </span>\n        {children ?? (\n          <span>\n            {context.percent}%\n            {context.variant === \"compact\" && context.level === \"full\" ? \" · Full\" : null}\n          </span>\n        )}\n      </Button>\n    </PopoverTrigger>\n  );\n}\n\nexport type ContextMeterContentProps = ComponentProps<\"div\">;\n\n/**\n * The breakdown. In ring and compact it is a 300px Popover that opens above the trigger; in bar\n * its children render in the card.\n */\nexport function ContextMeterContent({ className, children, ...props }: ContextMeterContentProps) {\n  const context = useContextMeter(\"ContextMeterContent\");\n  if (context.variant === \"bar\") return <>{children}</>;\n  return (\n    <PopoverContent\n      side=\"top\"\n      align=\"end\"\n      sideOffset={8}\n      data-slot=\"context-meter-content\"\n      aria-label=\"Context window\"\n      className={cn(\n        \"grid w-[300px] max-w-[calc(100vw-24px)] origin-bottom-right gap-3 rounded-[14px] border-0 bg-popover p-3.5 text-[13px]/[18px] text-popover-foreground shadow-[0_0_0_1px_var(--border-strong),0_16px_32px_-12px_oklch(0_0_0/0.32),0_4px_8px_-4px_oklch(0_0_0/0.12)] data-[side=bottom]:slide-in-from-top-0 data-[side=top]:slide-in-from-bottom-0 data-[state=open]:zoom-in-96 data-open:zoom-in-96 data-[state=open]:duration-180 data-open:duration-180 data-[state=open]:ease-[cubic-bezier(0.16,1,0.3,1)] data-open:ease-[cubic-bezier(0.16,1,0.3,1)] data-[state=closed]:zoom-out-96 data-closed:zoom-out-96 motion-reduce:zoom-in-100 motion-reduce:animate-none\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </PopoverContent>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Header, summary, status\n * -----------------------------------------------------------------------------------------------*/\n\nexport function ContextMeterHeader({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"context-meter-header\"\n      className={cn(\"flex min-w-0 items-center justify-between gap-3\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function ContextMeterTitle({ children, className, ...props }: ComponentProps<\"h3\">) {\n  const context = useContextMeter(\"ContextMeterTitle\");\n  return (\n    <h3\n      data-slot=\"context-meter-title\"\n      className={cn(\"m-0 text-[13px]/[18px] font-medium text-foreground\", className)}\n      {...props}\n    >\n      {children ?? (context.variant === \"bar\" ? \"Context\" : \"Context window\")}\n    </h3>\n  );\n}\n\n/** Bar: the big numeral and \"of 200k tokens · 84%\". Ring and compact: \"76.4k / 200k\". */\nexport function ContextMeterSummary({\n  className,\n  ...props\n}: Omit<ComponentProps<\"p\">, \"children\">) {\n  const context = useContextMeter(\"ContextMeterSummary\");\n  const used = context.format(context.used);\n  const limit = context.format(context.limit);\n  if (context.variant !== \"bar\") {\n    return (\n      <p\n        data-slot=\"context-meter-summary\"\n        className={cn(\"m-0 text-[12px]/4 text-subtle-foreground tabular-nums\", className)}\n        {...props}\n      >\n        <NumberTick value={used} /> / {limit}\n      </p>\n    );\n  }\n  return (\n    <p\n      data-slot=\"context-meter-summary\"\n      className={cn(\"m-0 flex min-w-0 flex-wrap items-baseline gap-x-1.5\", className)}\n      {...props}\n    >\n      <span className=\"text-[24px]/7 font-semibold tracking-[-0.01em] text-foreground tabular-nums\">\n        <NumberTick value={used} />\n      </span>\n      <span className=\"text-[12px]/4 text-subtle-foreground tabular-nums\">\n        of {limit} tokens · <NumberTick value={`${context.percent}%`} />\n      </span>\n    </p>\n  );\n}\n\n/** Nearing limit / Full / Compacting / Compacted. Renders nothing in the normal state. */\nexport function ContextMeterStatus({ className, children, ...props }: ComponentProps<\"span\">) {\n  const context = useContextMeter(\"ContextMeterStatus\");\n  const kind =\n    context.status === \"compacting\"\n      ? \"compacting\"\n      : context.status === \"compacted\"\n        ? \"compacted\"\n        : context.level === \"full\"\n          ? \"full\"\n          : context.level === \"warning\"\n            ? \"warning\"\n            : null;\n  if (!kind) return null;\n  return (\n    <Badge\n      key={kind}\n      variant=\"secondary\"\n      data-slot=\"context-meter-status\"\n      data-kind={kind}\n      className={cn(\n        \"h-6 shrink-0 animate-in gap-1.5 rounded-full border-0 px-2.5 py-0 text-[11.5px] font-medium duration-180 ease-[cubic-bezier(0.16,1,0.3,1)] fade-in-0 zoom-in-96 motion-reduce:animate-none\",\n        kind === \"warning\" && \"bg-warning/14 text-warning-ink\",\n        kind === \"full\" && \"bg-destructive/14 text-destructive-ink\",\n        kind === \"compacting\" && \"bg-muted text-muted-foreground\",\n        kind === \"compacted\" && \"bg-success/14 text-success-ink\",\n        className,\n      )}\n      {...props}\n    >\n      {kind === \"compacting\" ? (\n        <LoaderCircle\n          className=\"size-3 motion-safe:animate-spin\"\n          strokeWidth={2}\n          aria-hidden=\"true\"\n        />\n      ) : kind === \"compacted\" ? (\n        <Check className=\"size-3\" strokeWidth={2.2} aria-hidden=\"true\" />\n      ) : (\n        <span aria-hidden=\"true\" className=\"size-1.5 rounded-full bg-current\" />\n      )}\n      {children ??\n        (kind === \"warning\" ? (\n          \"Nearing limit\"\n        ) : kind === \"full\" ? (\n          \"Full\"\n        ) : kind === \"compacting\" ? (\n          <span className=\"shimmer-text\">Compacting</span>\n        ) : (\n          \"Compacted\"\n        ))}\n    </Badge>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Bar and legend\n * -----------------------------------------------------------------------------------------------*/\n\n/** Tonal ink, never hue: 88, 62, 42 and 26% foreground, cycling. */\nconst TONES = [\"bg-foreground/88\", \"bg-foreground/62\", \"bg-foreground/42\", \"bg-foreground/26\"];\n/** Segment stagger: 40ms each, first six only. */\nconst STAGGER = [\n  \"delay-0\",\n  \"delay-[40ms]\",\n  \"delay-[80ms]\",\n  \"delay-[120ms]\",\n  \"delay-[160ms]\",\n  \"delay-[200ms]\",\n];\n\n/**\n * The stacked 8px bar. Each segment is a full-width layer scaled from the left to where it ends,\n * so value changes animate `transform` only; 2px card-colored separators mark the boundaries.\n * Threshold ticks sit at `warnAt` and `fullAt`, and the freed region is hatched after compaction.\n */\nexport function ContextMeterBar({ className, ...props }: Omit<ComponentProps<\"div\">, \"children\">) {\n  const context = useContextMeter(\"ContextMeterBar\");\n  const { limit } = context;\n  const meter = context.variant === \"bar\";\n  let end = 0;\n  const layers = context.segments.map((segment, index) => {\n    end += Math.max(0, segment.tokens);\n    return { segment, index, end: limit > 0 ? Math.min(1, end / limit) : 0 };\n  });\n  const usedEnd = limit > 0 ? Math.min(1, context.used / limit) : 0;\n  const freedWidth =\n    context.status === \"compacted\" && context.freed && limit > 0\n      ? Math.min(1 - usedEnd, context.freed / limit)\n      : 0;\n  return (\n    <div\n      data-slot=\"context-meter-bar\"\n      className={cn(\"relative h-2 w-full\", className)}\n      {...(meter\n        ? {\n            role: \"meter\",\n            \"aria-label\": \"Context window\",\n            \"aria-valuemin\": 0,\n            \"aria-valuemax\": limit,\n            \"aria-valuenow\": Math.min(context.used, limit),\n            \"aria-valuetext\": context.valueText,\n          }\n        : { \"aria-hidden\": true })}\n      {...props}\n    >\n      <div className=\"absolute inset-0 isolate overflow-hidden rounded-full bg-muted\">\n        {layers.map(({ segment, index, end: layerEnd }) => {\n          const shimmering =\n            context.status === \"compacting\" && segment.id === context.compactSegment;\n          return (\n            <span\n              key={segment.id}\n              data-segment={segment.id}\n              className={cn(\n                \"absolute inset-0 origin-left [transition:transform_300ms_cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\",\n                STAGGER[Math.min(index, STAGGER.length - 1)],\n                shimmering\n                  ? \"bg-[linear-gradient(90deg,color-mix(in_oklab,var(--foreground)_26%,transparent),color-mix(in_oklab,var(--foreground)_55%,transparent),color-mix(in_oklab,var(--foreground)_26%,transparent))] bg-size-[200%_100%] motion-safe:animate-shimmer\"\n                  : TONES[index % TONES.length],\n              )}\n              style={{ transform: `scaleX(${layerEnd})`, zIndex: layers.length - index }}\n            />\n          );\n        })}\n        {layers.slice(0, -1).map(({ segment, end: layerEnd }) => (\n          <span\n            key={`gap-${segment.id}`}\n            className=\"absolute inset-0 z-20 [transition:transform_300ms_cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\"\n            style={{ transform: `translateX(${layerEnd * 100}%)` }}\n          >\n            <span className=\"absolute inset-y-0 -left-0.5 w-0.5 bg-card\" />\n          </span>\n        ))}\n        {freedWidth > 0 ? (\n          <span\n            data-slot=\"context-meter-freed-region\"\n            className=\"absolute inset-y-0 z-10 animate-in rounded-r-full bg-[repeating-linear-gradient(135deg,color-mix(in_oklab,var(--success)_30%,transparent)_0_2px,transparent_2px_5px)] shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--success)_45%,transparent)] delay-120 duration-240 fade-in-0 fill-mode-both motion-reduce:animate-none\"\n            style={{ left: `${usedEnd * 100}%`, width: `${freedWidth * 100}%` }}\n          />\n        ) : null}\n      </div>\n      {context.level !== \"empty\" && context.level !== \"normal\"\n        ? [context.warnAt, context.fullAt].map((at) => (\n            <span\n              key={at}\n              aria-hidden=\"true\"\n              className=\"absolute -top-[3px] h-3.5 w-px bg-border-strong\"\n              style={{ left: `${at * 100}%` }}\n            />\n          ))\n        : null}\n    </div>\n  );\n}\n\n/** One row per segment, then Free. Each row reads \"Files, 18,700 tokens, 9 percent\". */\nexport function ContextMeterLegend({\n  className,\n  ...props\n}: Omit<ComponentProps<\"ul\">, \"children\">) {\n  const context = useContextMeter(\"ContextMeterLegend\");\n  if (context.level === \"empty\") {\n    return (\n      <p\n        data-slot=\"context-meter-legend\"\n        className={cn(\"m-0 text-[12px]/4 text-subtle-foreground\", className)}\n      >\n        Nothing in context yet\n      </p>\n    );\n  }\n  const free = Math.max(0, context.limit - context.used);\n  const pct = (tokens: number) =>\n    context.limit > 0 ? Math.round((tokens / context.limit) * 100) : 0;\n  const rows = [\n    ...context.segments.map((segment, index) => ({\n      id: segment.id,\n      label: segment.label,\n      tokens: segment.tokens,\n      swatch: TONES[index % TONES.length] ?? \"\",\n    })),\n    {\n      id: \"free\",\n      label: \"Free\",\n      tokens: free,\n      swatch: \"bg-muted shadow-[inset_0_0_0_1px_var(--border-strong)]\",\n    },\n  ];\n  return (\n    <ul\n      data-slot=\"context-meter-legend\"\n      className={cn(\"m-0 grid list-none gap-[7px] p-0\", className)}\n      {...props}\n    >\n      {rows.map((row) => (\n        <li\n          key={row.id}\n          className=\"grid grid-cols-[8px_minmax(0,1fr)_auto_30px] items-center gap-2 text-[12px]/4 text-muted-foreground\"\n        >\n          <span aria-hidden=\"true\" className={cn(\"size-2 rounded-[2px]\", row.swatch)} />\n          <span className=\"sr-only\">\n            {`${row.label}, ${fullNumber.format(row.tokens)} tokens, ${pct(row.tokens)} percent`}\n          </span>\n          <span aria-hidden=\"true\" className=\"truncate\">\n            {row.label}\n          </span>\n          <span aria-hidden=\"true\" className=\"text-right font-medium text-foreground tabular-nums\">\n            <NumberTick value={context.format(row.tokens)} />\n          </span>\n          <span aria-hidden=\"true\" className=\"text-right text-subtle-foreground tabular-nums\">\n            {pct(row.tokens)}%\n          </span>\n        </li>\n      ))}\n    </ul>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Actions row: hint, compacting caption, freed line, Compact now and Undo\n * -----------------------------------------------------------------------------------------------*/\n\nexport function ContextMeterActions({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useContextMeter(\"ContextMeterActions\");\n  return (\n    <div\n      data-slot=\"context-meter-actions\"\n      className={cn(\n        context.variant === \"bar\"\n          ? \"flex min-w-0 items-center justify-between gap-2.5\"\n          : \"grid min-w-0 justify-items-start gap-2.5 border-t border-border pt-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/**\n * \"Summarizes older turns at 90%.\" (popover: \"Older turns are summarized automatically at 90%.\").\n * Shows only when `autoCompactAt` is set and nothing is compacting or compacted.\n */\nexport function ContextMeterHint({ className, children, ...props }: ComponentProps<\"p\">) {\n  const context = useContextMeter(\"ContextMeterHint\");\n  if (context.status !== \"idle\") return null;\n  if (children === undefined && context.autoCompactAt === undefined) return null;\n  const at = Math.round((context.autoCompactAt ?? 0) * 100);\n  return (\n    <p\n      data-slot=\"context-meter-hint\"\n      className={cn(\"m-0 text-[11.5px]/4 text-subtle-foreground\", className)}\n      {...props}\n    >\n      {children ??\n        (context.variant === \"bar\"\n          ? `Summarizes older turns at ${at}%.`\n          : `Older turns are summarized automatically at ${at}%.`)}\n    </p>\n  );\n}\n\n/** The consumer's caption while compacting, for example \"Summarizing 38 earlier turns…\". */\nexport function ContextMeterCompacting({ className, children, ...props }: ComponentProps<\"p\">) {\n  const context = useContextMeter(\"ContextMeterCompacting\");\n  if (context.status !== \"compacting\") return null;\n  return (\n    <p\n      data-slot=\"context-meter-compacting\"\n      className={cn(\"m-0 text-[12px]/4 shimmer-text\", className)}\n      {...props}\n    >\n      {children ?? \"Summarizing earlier turns…\"}\n    </p>\n  );\n}\n\n/** After compaction: \"Freed 61.8k tokens\" plus the consumer's detail, such as \"38 turns → 1 summary\". */\nexport function ContextMeterFreed({ className, children, ...props }: ComponentProps<\"p\">) {\n  const context = useContextMeter(\"ContextMeterFreed\");\n  if (context.status !== \"compacted\" || context.freed === undefined) return null;\n  return (\n    <p\n      data-slot=\"context-meter-freed\"\n      className={cn(\n        \"m-0 animate-in text-[11.5px]/4 text-subtle-foreground duration-240 fade-in-0 motion-reduce:animate-none\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"font-medium text-success-ink\">\n        Freed <NumberTick value={context.format(context.freed)} /> tokens\n      </span>\n      {children ? <> · {children}</> : null}\n    </p>\n  );\n}\n\nconst actionClass =\n  \"h-7 shrink-0 gap-1.5 rounded-full border-0 bg-secondary px-3 text-[12px] font-medium text-foreground [transition:background-color_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-accent focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.97] has-[>svg]:px-3 motion-reduce:transition-none motion-reduce:active:scale-100\";\n\n/** \"Compact now\": renders when `onCompact` is set and nothing is compacting or compacted. */\nexport function ContextMeterCompactAction({\n  className,\n  children,\n  onClick,\n  ...props\n}: ComponentProps<\"button\">) {\n  const context = useContextMeter(\"ContextMeterCompactAction\");\n  if (!context.onCompact || context.status !== \"idle\" || context.level === \"empty\") return null;\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"context-meter-compact\"\n      className={cn(actionClass, className)}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.onCompact?.();\n      }}\n      {...props}\n    >\n      <FoldVertical className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />\n      {children ?? \"Compact now\"}\n    </Button>\n  );\n}\n\n/** \"Undo\": renders only after compaction, and only when `onUndoCompact` is set. */\nexport function ContextMeterUndoAction({\n  className,\n  children,\n  onClick,\n  ...props\n}: ComponentProps<\"button\">) {\n  const context = useContextMeter(\"ContextMeterUndoAction\");\n  if (!context.onUndoCompact || context.status !== \"compacted\") return null;\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"context-meter-undo\"\n      className={cn(actionClass, className)}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.onUndoCompact?.();\n      }}\n      {...props}\n    >\n      <Undo2 className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />\n      {children ?? \"Undo\"}\n    </Button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/context-meter.tsx"
    }
  ],
  "categories": [
    "ai",
    "agents"
  ],
  "type": "registry:ui"
}