{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gauge",
  "type": "registry:ui",
  "title": "Gauge",
  "description": "One value against a range on a semicircle of fine ticks, with the label, the figure and a caption in the opening. The ticks up to the value take the foreground or one data hue, and the last, longer tick marks the value. Static, like the meter.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/gauge.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/gauge 2026.11.05. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Gauge: one value against a range, on a semicircle of fine ticks, with the figure in the opening. A sibling of\n * the Meter in chart.tsx for a place where the number is the point: credits left, storage used.\n *\n * - Marks: `ticks` radial ticks (48 by default) spread over 180 degrees, each 2px wide with square ends. The ticks\n *   up to the value are the hue's mark tone, or `--litt-fg` without a hue (the quiet default); the rest are `--litt-fg` at\n *   12%. The last filled tick marks the value: it reaches 4px further out and takes the hue's -text tone (or\n *   `--litt-fg`), which is what a needle would have done. No needle, no glow, no gradient (R2, R7).\n * - Opening: the label above (caption size, fg-subtle, sentence case), the figure (the sans at the h2 size, fg,\n *   tabular numbers) and an optional caption below, centered at the foot of the arc. It is HTML over the SVG, so\n *   the text stays crisp whatever the SVG scales to.\n * - Size: `size` is the width in px. The SVG keeps a viewBox of that width, so ticks keep their 2px at the\n *   natural size and the whole drawing scales down to fit a narrower container. It never exceeds its container.\n *   The tick length follows the size. Under about 200px there is room for a short label and the figure, not a caption.\n * - Color: a hue is one meaning (R7): here, the quantity itself. Without one the gauge is foreground only.\n * - Semantics: role=\"meter\" with the label as its name, a min, a max, a now and the value text; the caption is its\n *   description. The ticks and the text over them are hidden from assistive tech, so nothing is read twice.\n * - Motion: none. Static, like the Meter: no entrance, no hover, no focus stop. A value that changes just changes.\n * - Fun mode (motion/fun.tsx, a trial, off unless a <FunMode> is on): the lit ticks sweep up to the value on arrival\n *   and to each new value, on the grow spring, so the marking tick runs a tick or two past and settles back like a\n *   needle with weight; the figure counts with the sweep and never shows more than the value. Still ticks lighting up:\n *   no needle, no glow. At rest it is the static Gauge exactly; reduced motion turns it off.\n */\nimport { useId, useState } from \"react\";\nimport { useMotionValueEvent } from \"motion/react\";\nimport { countText, useFun, useFunValue } from \"../../lib/litt/motion\";\n\n/** The five data hues, in their fixed order (R7). */\nexport type GaugeHue = \"blue\" | \"amber\" | \"teal\" | \"violet\" | \"rose\";\n\n/** The mark tone fills the ticks up to the value. The -text tone is the stronger one that marks the value itself. */\nconst HUE: Record<GaugeHue, { mark: string; value: string }> = {\n  blue: { mark: \"stroke-litt-data-blue\", value: \"stroke-litt-data-blue-text\" },\n  amber: { mark: \"stroke-litt-data-amber\", value: \"stroke-litt-data-amber-text\" },\n  teal: { mark: \"stroke-litt-data-teal\", value: \"stroke-litt-data-teal-text\" },\n  violet: { mark: \"stroke-litt-data-violet\", value: \"stroke-litt-data-violet-text\" },\n  rose: { mark: \"stroke-litt-data-rose\", value: \"stroke-litt-data-rose-text\" },\n};\nconst QUIET = { mark: \"stroke-litt-fg\", value: \"stroke-litt-fg\" };\nconst REST = \"stroke-[color-mix(in_oklab,var(--litt-fg)_12%,transparent)]\";\n\nconst TICK = 2; // tick width in px\nconst EDGE = 6; // room between the arc and the box: the marker reaches 4px past the arc, and a tick has half its width\nconst REACH = 4; // how much further out the marker tick reaches\nconst OPENING_GAP = 8; // air between the inner ends of the ticks and the text\n\nexport type GaugeProps = {\n  /** What is measured: \"Credits left\". It names the meter for assistive tech and sits above the figure. */\n  label: string;\n  /** The value, from `min` to `max`. A value outside the range is clamped. */\n  value: number;\n  /** The value at the start of the arc. */\n  min?: number;\n  /** The value at the end of the arc. */\n  max?: number;\n  /** The reading in words, which the meter announces: \"20,830 credits\" or \"42 of 100 GB\". It is also the figure unless you pass one. */\n  valueText: string;\n  /** The big figure when it should read shorter than the value text, such as \"42 GB\" for \"42 of 100 GB\". */\n  figure?: string;\n  /** A line under the figure in fg-subtle, such as \"2k a day on average\". It describes the meter. Leave it out under about 200px. */\n  caption?: string;\n  /** Colors the ticks up to the value with one of the five data hues. Leave it out for the quiet foreground default. */\n  hue?: GaugeHue;\n  /** The width in px. The gauge scales down to fit a narrower container and never exceeds it. */\n  size?: number;\n  /** How many ticks make up the arc. */\n  ticks?: number;\n  /** Extra classes for the gauge, such as a margin. */\n  className?: string;\n};\n\nconst round = (n: number) => Math.round(n * 100) / 100;\n\n/** One semicircle of fine ticks with the figure in its opening. Static: it never animates. */\nexport function Gauge({ label, value, min = 0, max = 100, valueText, figure, caption, hue, size = 240, ticks = 48, className = \"\" }: GaugeProps) {\n  const captionId = useId();\n  const count = Math.max(2, Math.round(ticks));\n  const span = max - min;\n  const share = span > 0 ? Math.max(0, Math.min(1, (value - min) / span)) : 0;\n  /* A value above the start always shows at least one tick; the last filled tick is the one that marks it. */\n  const filled = share <= 0 ? 0 : Math.max(1, Math.round(share * count));\n  const now = span > 0 ? Math.max(min, Math.min(max, value)) : min;\n\n  /* Fun mode: the ticks drawn while the sweep moves, and the figure counting with it. Off or still, the value's own. */\n  const fun = useFun();\n  const sweep = useFunValue(filled, { count: value });\n  /* The ticks follow the sweep through its overshoot and settle, after the figure has stopped counting. */\n  const [swept, setSwept] = useState(filled);\n  useMotionValueEvent(sweep.value, \"change\", (t) => setSwept(Math.max(0, Math.min(count, Math.round(t)))));\n  const lit = fun && sweep.moving ? swept : filled;\n  const shownFigure = fun ? countText(figure ?? valueText, value, sweep.shown) : (figure ?? valueText);\n\n  const half = size / 2;\n  const outer = half - EDGE;\n  const length = Math.max(8, Math.round(size / 16));\n  const inner = outer - length;\n  const tick = (i: number, from: number, to: number) => {\n    const angle = Math.PI * (1 - i / (count - 1));\n    const cos = Math.cos(angle);\n    const sin = Math.sin(angle);\n    return `M${round(half + from * cos)} ${round(half - from * sin)}L${round(half + to * cos)} ${round(half - to * sin)}`;\n  };\n  const run = (from: number, to: number) => {\n    let d = \"\";\n    for (let i = from; i < to; i++) d += tick(i, inner, outer);\n    return d;\n  };\n\n  const tone = hue ? HUE[hue] : QUIET;\n  /* The opening: the text keeps clear of the inner ends of the ticks. A share of the width, so it scales with the SVG. */\n  const pad = `${round(((EDGE + length + OPENING_GAP) / size) * 100)}%`;\n\n  return (\n    <div\n      role=\"meter\"\n      aria-label={label}\n      aria-valuemin={min}\n      aria-valuemax={max}\n      aria-valuenow={now}\n      aria-valuetext={valueText}\n      aria-describedby={caption ? captionId : undefined}\n      className={\"relative w-full \" + className}\n      style={{ maxWidth: size }}\n    >\n      <svg viewBox={`0 0 ${size} ${Math.ceil(half) + 1}`} width={size} height={Math.ceil(half) + 1} aria-hidden=\"true\" focusable=\"false\" className=\"block h-auto w-full\">\n        <g fill=\"none\" strokeWidth={TICK} strokeLinecap=\"butt\">\n          <path d={run(lit, count)} className={REST} />\n          {lit > 1 && <path d={run(0, lit - 1)} className={tone.mark} />}\n          {lit > 0 && <path d={tick(lit - 1, inner, outer + REACH)} className={tone.value} />}\n        </g>\n      </svg>\n      <div aria-hidden=\"true\" className=\"absolute inset-x-0 bottom-0 flex flex-col items-center gap-1 text-center\" style={{ paddingInline: pad }}>\n        <span className=\"text-litt-caption text-litt-fg-subtle litt-legend\">{label}</span>\n        <span className=\"text-litt-h2 tabular-nums text-litt-fg\">{shownFigure}</span>\n        {caption && (\n          <span id={captionId} className=\"text-litt-caption font-normal text-litt-fg-subtle\">\n            {caption}\n          </span>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "target": "components/litt/gauge.tsx"
    }
  ],
  "docs": "Import Gauge from components/litt/gauge. Pass a label, a value, a max and a value text (what a screen reader hears); add a figure, a caption or a hue. It scales down to its container.",
  "meta": {
    "version": "2026.11.05",
    "released": "2026-11-05",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
