{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Context window meter whose ring fills and shifts hue at the warning threshold, never changing size.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst VIEWBOX = 32;\nconst CENTER = VIEWBOX / 2;\nconst RADIUS = 13;\nconst STROKE_WIDTH = 3;\n/** Fraction of the window at which the ring changes hue. */\nconst DEFAULT_WARNING_AT = 0.8;\nconst DEFAULT_DANGER_AT = 0.95;\nconst WARNING_COLOR = \"oklch(78% 0.16 75)\";\nconst DANGER_COLOR = \"oklch(63% 0.21 25)\";\n\nexport type AIContextBreakdownItem = {\n  label: string;\n  tokens: number;\n};\n\nexport type AIContextMeterProps = {\n  /** Optional split of what is filling the window. */\n  breakdown?: AIContextBreakdownItem[];\n  className?: string;\n  /** Fraction at which the ring turns red. */\n  dangerAt?: number;\n  /** Total size of the context window, in tokens. */\n  limit: number;\n  /** Tokens currently used. */\n  used: number;\n  /** Fraction at which the ring turns amber. */\n  warningAt?: number;\n};\n\nconst COMPACT_THRESHOLD = 1000;\nconst MILLION = 1_000_000;\n\n/** Below this many thousands, keep a decimal — \"2k\" for 1,800 is a lie. */\nconst DECIMAL_BELOW = 10 * COMPACT_THRESHOLD;\n\nconst formatTokens = (tokens: number): string => {\n  if (tokens >= MILLION) {\n    return `${(tokens / MILLION).toFixed(1)}M`;\n  }\n  if (tokens >= DECIMAL_BELOW) {\n    return `${Math.round(tokens / COMPACT_THRESHOLD)}k`;\n  }\n  if (tokens >= COMPACT_THRESHOLD) {\n    return `${(tokens / COMPACT_THRESHOLD).toFixed(1)}k`;\n  }\n  return String(tokens);\n};\n\n/**\n * How much of the context window is gone.\n *\n * Crossing a threshold changes the **hue**, never the size. Growing the ring at\n * the warning point would read as progress — as something filling up nicely —\n * which is the opposite of the message. The geometry stays put and the colour\n * does the talking.\n */\nconst AIContextMeter = ({\n  breakdown,\n  className,\n  dangerAt = DEFAULT_DANGER_AT,\n  limit,\n  used,\n  warningAt = DEFAULT_WARNING_AT,\n}: AIContextMeterProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isOpen, setIsOpen] = useState(false);\n\n  const fraction = limit > 0 ? Math.min(1, Math.max(0, used / limit)) : 0;\n  const percent = Math.round(fraction * 100);\n\n  const color = (() => {\n    if (fraction >= dangerAt) {\n      return DANGER_COLOR;\n    }\n    if (fraction >= warningAt) {\n      return WARNING_COLOR;\n    }\n    return \"currentColor\";\n  })();\n\n  const hasBreakdown = Boolean(breakdown?.length);\n\n  return (\n    <div className={cn(\"relative inline-block\", className)}>\n      <button\n        aria-expanded={hasBreakdown ? isOpen : undefined}\n        aria-label={`Context window ${percent}% used, ${formatTokens(used)} of ${formatTokens(limit)} tokens`}\n        className=\"flex cursor-pointer items-center gap-1.5 rounded-lg px-1 py-0.5 text-muted-foreground text-xs transition-colors hover:text-foreground\"\n        disabled={!hasBreakdown}\n        onBlur={() => setIsOpen(false)}\n        onClick={() => setIsOpen((current) => !current)}\n        onFocus={() => hasBreakdown && setIsOpen(true)}\n        onMouseEnter={() => hasBreakdown && setIsOpen(true)}\n        onMouseLeave={() => setIsOpen(false)}\n        type=\"button\"\n      >\n        <svg\n          aria-hidden=\"true\"\n          className=\"size-4 -rotate-90\"\n          viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}\n        >\n          <circle\n            cx={CENTER}\n            cy={CENTER}\n            fill=\"none\"\n            r={RADIUS}\n            stroke=\"currentColor\"\n            strokeOpacity={0.2}\n            strokeWidth={STROKE_WIDTH}\n          />\n          {/* pathLength normalises the dash maths, so the fill is just the\n              fraction — no circumference arithmetic to get wrong. */}\n          <motion.circle\n            animate={{\n              stroke: color,\n              strokeDasharray: `${fraction} ${1 - fraction}`,\n            }}\n            cx={CENTER}\n            cy={CENTER}\n            fill=\"none\"\n            pathLength={1}\n            r={RADIUS}\n            strokeLinecap=\"round\"\n            strokeWidth={STROKE_WIDTH}\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n          />\n        </svg>\n\n        <span className=\"tabular-nums\">\n          {formatTokens(used)}/{formatTokens(limit)}\n        </span>\n      </button>\n\n      <AnimatePresence>\n        {isOpen && hasBreakdown ? (\n          <motion.div\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            className=\"absolute bottom-full left-0 z-50 mb-1.5 w-56 rounded-xl border border-border bg-background p-2.5 shadow-lg\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, scale: 0.96, y: 4 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1, y: 0 }\n                : { opacity: 0, scale: 0.96, y: 4 }\n            }\n            style={{ transformOrigin: \"bottom left\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.18, ease: EASE_OUT }\n            }\n          >\n            <ul className=\"list-none space-y-1\">\n              {breakdown?.map((item) => (\n                <li\n                  className=\"flex items-baseline justify-between gap-3 text-xs\"\n                  key={item.label}\n                >\n                  <span className=\"truncate text-muted-foreground\">\n                    {item.label}\n                  </span>\n                  <span className=\"shrink-0 text-foreground tabular-nums\">\n                    {formatTokens(item.tokens)}\n                  </span>\n                </li>\n              ))}\n            </ul>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nexport default AIContextMeter;\n","path":"index.tsx","target":"components/smoothui/ai-context-meter/index.tsx","type":"registry:ui"}],"name":"ai-context-meter","registryDependencies":[],"title":"Ai Context Meter","type":"registry:ui"}