{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gauge",
  "title": "Gauge",
  "author": "Thordur <https://github.com/thordursk>",
  "description": "Composable SVG gauge primitives: track, arc, zones, ticks, tick fills, labels, needle, hub, dot, tooltip and value text, with optional spring or tween animation and nested gauges, as round dials or horizontal and vertical bars. No runtime dependencies.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/gauge/index.ts",
      "content": "export { Gauge, type GaugeFit, type GaugeProps } from \"./gauge\"\nexport {\n  LinearGauge,\n  type LinearGaugePadding,\n  type LinearGaugeProps,\n} from \"./linear\"\nexport {\n  GaugeInset,\n  LinearGaugeInset,\n  type GaugeInsetProps,\n  type LinearGaugeInsetProps,\n} from \"./inset\"\nexport {\n  useGauge,\n  useRoundGauge,\n  type GaugeContextValue,\n  type GaugeDomainProps,\n  type GaugeLinearShape,\n  type GaugeRange,\n  type GaugeReading,\n  type GaugeReadingProps,\n  type GaugeRoundShape,\n  type LinearGaugeDomainProps,\n} from \"./context\"\nexport {\n  arcScale,\n  lineScale,\n  type GaugeBox,\n  type GaugeOrientation,\n  type GaugePoint,\n  type GaugeScale,\n} from \"./scale\"\nexport {\n  GaugeTrack,\n  GaugeArc,\n  GaugeStack,\n  GaugeZones,\n  type GaugeArcProps,\n  type GaugeStackPart,\n  type GaugeStackProps,\n  type GaugeZonesProps,\n} from \"./arcs\"\nexport { GaugeControl, type GaugeControlProps } from \"./control\"\nexport {\n  GaugeTicks,\n  GaugeTickFill,\n  GaugeMarks,\n  GaugeTickLabels,\n  type GaugeTicksProps,\n  type GaugeTickFillProps,\n  type GaugeMarksProps,\n  type GaugeTickLabelsProps,\n} from \"./ticks\"\nexport {\n  GaugeText,\n  GaugeValue,\n  type GaugeTextProps,\n  type GaugeValueProps,\n} from \"./text\"\nexport { GaugeDot, type GaugeDotProps } from \"./dot\"\nexport {\n  GaugeTooltip,\n  type GaugeTooltipPosition,\n  type GaugeTooltipProps,\n  type GaugeTooltipSide,\n} from \"./tooltip\"\nexport {\n  GaugeNeedle,\n  GaugeHub,\n  type GaugeNeedleProps,\n  type GaugeHubProps,\n} from \"./needle\"\nexport {\n  angleToValue,\n  arcBox,\n  arcPath,\n  clamp,\n  cutValues,\n  degreesToRadians,\n  foldValue,\n  polar,\n  stepValues,\n  valueToAngle,\n  valueToTurnedAngle,\n} from \"./math\"\nexport {\n  clockLabel,\n  clockTime,\n  compassLabel,\n  durationLabel,\n  fadeColor,\n  fontClass,\n  weightClass,\n  zoneColor,\n  zoneCutoffs,\n  type FontFamily,\n  type FontWeight,\n  type GaugeEnd,\n  type NeedleStyle,\n  type StrokeCap,\n  type TextAnchor,\n  type ValueColor,\n  type Zone,\n} from \"./utils\"\nexport {\n  DEFAULT_TRANSITION,\n  cubicBezier,\n  easingFunction,\n  resolveTransition,\n  springPhysics,\n  stepSpring,\n  type GaugeEasing,\n  type GaugeSpringTransition,\n  type GaugeTransition,\n  type GaugeTweenTransition,\n} from \"./transition\"\nexport { useAnimatedValue } from \"./use-animated-value\"\n",
      "type": "registry:ui",
      "target": "@components/gauge/index.ts"
    },
    {
      "path": "components/gauge/gauge.tsx",
      "content": "\"use client\"\n\nimport type { SVGProps } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { arcBox } from \"./math\"\n\nimport { GaugeContext, useGaugeContextValue } from \"./context\"\nimport type { GaugeDomainProps } from \"./context\"\n\n/** See `GaugeProps.fit`. */\nexport type GaugeFit = \"square\" | \"content\"\n\nexport type GaugeProps = Omit<SVGProps<SVGSVGElement>, \"viewBox\"> &\n  GaugeDomainProps & {\n    /** Extra room around the reference circle for thick strokes, ticks and labels. */\n    padding?: number\n    /**\n     * How the SVG box is sized. `square` boxes the whole circle, so a half gauge\n     * reserves the room a full one would. `content` fits the box to the arc's own\n     * sweep and sets a matching `aspect-ratio`, so the dial fills the width it is\n     * given. `padding` is still what makes room for ticks, labels and value text,\n     * since those are drawn by children the root cannot measure.\n     */\n    fit?: GaugeFit\n  }\n\n/**\n * Root of a composable gauge. Renders a square SVG centred on the origin and\n * publishes the value, domain and geometry to any primitive rendered inside.\n */\nexport const Gauge = ({\n  value,\n  min,\n  max,\n  startAngle,\n  endAngle,\n  radius,\n  transition,\n  initialValue,\n  wrap,\n  rotate,\n  padding = 48,\n  fit = \"square\",\n  className,\n  style,\n  children,\n  ...props\n}: GaugeProps) => {\n  const gauge = useGaugeContextValue({\n    value,\n    min,\n    max,\n    startAngle,\n    endAngle,\n    radius,\n    transition,\n    initialValue,\n    wrap,\n    rotate,\n  })\n\n  const half = gauge.radius + padding\n  const box =\n    fit === \"content\"\n      ? arcBox(gauge.radius, gauge.startAngle, gauge.endAngle, padding)\n      : { x: -half, y: -half, width: 2 * half, height: 2 * half }\n\n  return (\n    <GaugeContext.Provider value={gauge}>\n      <svg\n        viewBox={`${box.x} ${box.y} ${box.width} ${box.height}`}\n        preserveAspectRatio=\"xMidYMid meet\"\n        /* A fitted box takes its height from its width, so it needs the ratio\n           on the element; a square one fills whatever box it is handed. */\n        style={\n          fit === \"content\"\n            ? { aspectRatio: `${box.width} / ${box.height}`, ...style }\n            : style\n        }\n        className={cn(\n          \"overflow-visible\",\n          fit === \"content\" ? \"max-h-full w-full\" : \"h-full w-full\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </svg>\n    </GaugeContext.Provider>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/gauge.tsx"
    },
    {
      "path": "components/gauge/linear.tsx",
      "content": "\"use client\"\n\nimport type { SVGProps } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport { GaugeContext, useLinearGaugeContextValue } from \"./context\"\nimport type { LinearGaugeDomainProps } from \"./context\"\n\n/** Room on each side of a bar, in SVG units. A side left out gets none. */\nexport type LinearGaugePadding =\n  number | { top?: number; right?: number; bottom?: number; left?: number }\n\nexport type LinearGaugeProps = Omit<SVGProps<SVGSVGElement>, \"viewBox\"> &\n  LinearGaugeDomainProps & {\n    /**\n     * Extra room round the bar for thick strokes, ticks, labels and value\n     * text, which the root cannot measure since its children draw them. One\n     * number for every side, or a side each: a readout above a horizontal\n     * bar and its labels below rarely need the same.\n     */\n    padding?: LinearGaugePadding\n  }\n\n/**\n * Root of a straight gauge: a horizontal or vertical bar. It publishes the\n * same context `Gauge` does with a straight scale in place of an arc, so\n * every primitive that draws along the scale — track, arc, stack, zones,\n * ticks, tick fill, marks, labels, dot, tooltip and text — works inside it\n * unchanged. Only the needle, which pivots about a centre, needs a round one.\n *\n * The bar runs `length` SVG units centred on the origin, left to right or\n * bottom to top. Offsets move a part outward: up from a horizontal bar, left\n * of a vertical one, so labels below or to the right take a negative offset\n * just as they sit inside a dial. The SVG takes its box from the bar and its\n * padding and sets a matching `aspect-ratio`, so a horizontal bar fills the\n * width it is given and a vertical one the height.\n */\nexport const LinearGauge = ({\n  value,\n  min,\n  max,\n  transition,\n  initialValue,\n  wrap,\n  orientation,\n  length,\n  padding = 32,\n  className,\n  style,\n  children,\n  ...props\n}: LinearGaugeProps) => {\n  const gauge = useLinearGaugeContextValue({\n    value,\n    min,\n    max,\n    transition,\n    initialValue,\n    wrap,\n    orientation,\n    length,\n  })\n\n  const pad =\n    typeof padding === \"number\"\n      ? { top: padding, right: padding, bottom: padding, left: padding }\n      : { top: 0, right: 0, bottom: 0, left: 0, ...padding }\n  const half = gauge.length / 2\n  const vertical = gauge.orientation === \"vertical\"\n  const box = vertical\n    ? {\n        x: -pad.left,\n        y: -half - pad.top,\n        width: pad.left + pad.right,\n        height: gauge.length + pad.top + pad.bottom,\n      }\n    : {\n        x: -half - pad.left,\n        y: -pad.top,\n        width: gauge.length + pad.left + pad.right,\n        height: pad.top + pad.bottom,\n      }\n\n  return (\n    <GaugeContext.Provider value={gauge}>\n      <svg\n        viewBox={`${box.x} ${box.y} ${box.width} ${box.height}`}\n        preserveAspectRatio=\"xMidYMid meet\"\n        style={{ aspectRatio: `${box.width} / ${box.height}`, ...style }}\n        className={cn(\n          \"overflow-visible\",\n          vertical ? \"h-full w-auto\" : \"h-auto w-full\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </svg>\n    </GaugeContext.Provider>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/linear.tsx"
    },
    {
      "path": "components/gauge/inset.tsx",
      "content": "\"use client\"\n\nimport type { SVGProps } from \"react\"\n\nimport {\n  GaugeContext,\n  useGaugeContextValue,\n  useLinearGaugeContextValue,\n} from \"./context\"\nimport type { GaugeDomainProps, LinearGaugeDomainProps } from \"./context\"\n\nexport type GaugeInsetProps = Omit<SVGProps<SVGGElement>, \"transform\"> &\n  GaugeDomainProps & {\n    /** Centre of the inset in the host gauge's SVG units. Positive y is down. */\n    x?: number\n    y?: number\n    /** Size relative to the host gauge: 0.3 draws it at a third of the size. */\n    scale?: number\n  }\n\n/**\n * A whole gauge drawn inside another one: a fuel dial under a speedometer, a\n * second ring inside the first. It takes its own value and domain and sets up\n * the same context `Gauge` does, so every primitive works inside it unchanged.\n *\n * It draws in the host's coordinate system rather than its own viewport, so it\n * needs no `padding` or `fit`; `x`, `y` and `scale` place it, and everything\n * inside — strokes, ticks, text — shrinks with it. Its reference radius stays\n * the host's by default, which makes `scale` the only size to reason about.\n */\nexport const GaugeInset = ({\n  value,\n  min,\n  max,\n  startAngle,\n  endAngle,\n  radius,\n  transition,\n  initialValue,\n  wrap,\n  rotate,\n  x = 0,\n  y = 0,\n  scale = 1,\n  children,\n  ...props\n}: GaugeInsetProps) => {\n  const gauge = useGaugeContextValue({\n    value,\n    min,\n    max,\n    startAngle,\n    endAngle,\n    radius,\n    transition,\n    initialValue,\n    wrap,\n    rotate,\n  })\n\n  return (\n    <GaugeContext.Provider value={gauge}>\n      <g transform={`translate(${x} ${y}) scale(${scale})`} {...props}>\n        {children}\n      </g>\n    </GaugeContext.Provider>\n  )\n}\n\nexport type LinearGaugeInsetProps = Omit<SVGProps<SVGGElement>, \"transform\"> &\n  LinearGaugeDomainProps & {\n    /** Centre of the bar in the host gauge's SVG units. Positive y is down. */\n    x?: number\n    y?: number\n    /** Size relative to the host gauge: 0.5 draws it at half the size. */\n    scale?: number\n  }\n\n/**\n * A bar drawn inside another gauge, as `GaugeInset` draws a dial: a fuel\n * bar under a speedometer, a battery level across the foot of a ring. It\n * takes its own value and domain and sets up the same context\n * `LinearGauge` does, placed by `x`, `y` and `scale`.\n */\nexport const LinearGaugeInset = ({\n  value,\n  min,\n  max,\n  transition,\n  initialValue,\n  wrap,\n  orientation,\n  length,\n  x = 0,\n  y = 0,\n  scale = 1,\n  children,\n  ...props\n}: LinearGaugeInsetProps) => {\n  const gauge = useLinearGaugeContextValue({\n    value,\n    min,\n    max,\n    transition,\n    initialValue,\n    wrap,\n    orientation,\n    length,\n  })\n\n  return (\n    <GaugeContext.Provider value={gauge}>\n      <g transform={`translate(${x} ${y}) scale(${scale})`} {...props}>\n        {children}\n      </g>\n    </GaugeContext.Provider>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/inset.tsx"
    },
    {
      "path": "components/gauge/context.tsx",
      "content": "\"use client\"\n\nimport { createContext, useContext } from \"react\"\n\nimport { clamp, foldValue, valueToAngle } from \"./math\"\nimport {\n  arcScale,\n  lineScale,\n  type GaugeOrientation,\n  type GaugeScale,\n} from \"./scale\"\nimport type { GaugeTransition } from \"./transition\"\nimport { useAnimatedValue } from \"./use-animated-value\"\n\n/**\n * A span of the domain rather than a single reading: a forecast's low and\n * high, a target band, a sleep window. Pass it as a gauge's `value` and the\n * arc fills between the two ends.\n */\nexport type GaugeRange = readonly [from: number, to: number]\n\n/** The reading and domain, the same on every gauge whatever its shape. */\nexport type GaugeReading = {\n  /** Value being drawn this frame, clamped to the domain. Animated when the\n      gauge has a transition. For a range, its far end. */\n  value: number\n  /** Value the gauge is heading for; equals `value` once settled. */\n  target: number\n  /** Where a range starts, animated alongside `value`. On a single value it\n      is `min`, so the stretch from here to `value` is the fill either way. */\n  from: number\n  /** Where `from` is heading for. */\n  targetFrom: number\n  /** Whether the gauge shows a range rather than a single value. */\n  range: boolean\n  /** Whether the domain is a closed ring, as the root's `wrap` sets. A range\n      on one runs forward from `from` to `value`, across the seam if need be. */\n  wrap: boolean\n  min: number\n  max: number\n}\n\n/** What a round `Gauge` adds: the angles and radius its scale is bent to. */\nexport type GaugeRoundShape = {\n  shape: \"round\"\n  startAngle: number\n  endAngle: number\n  /** Radius of the gauge's reference circle in SVG units. */\n  radius: number\n  /** Maps a domain value onto a gauge angle. The same as `positionOf`. */\n  angleOf: (value: number) => number\n}\n\n/** What a `LinearGauge` adds: which way its bar runs, and how long it is. */\nexport type GaugeLinearShape = {\n  shape: \"linear\"\n  orientation: GaugeOrientation\n  /** Length of the bar in SVG units, end to end. */\n  length: number\n}\n\nexport type GaugeContextValue = GaugeReading & {\n  /** The line the domain is laid out along, which every part that draws\n      along it asks for its geometry. */\n  scale: GaugeScale\n  /** Maps a domain value onto a position on the scale. */\n  positionOf: (value: number) => number\n} & (GaugeRoundShape | GaugeLinearShape)\n\nexport const GaugeContext = createContext<GaugeContextValue | null>(null)\n\nexport const useGauge = () => {\n  const ctx = useContext(GaugeContext)\n  if (!ctx) {\n    throw new Error(\"Gauge primitives must be rendered inside <Gauge>.\")\n  }\n  return ctx\n}\n\n/**\n * The context of a round gauge, for the parts that only mean anything on\n * one: a needle has to pivot about a centre, which a bar has none of.\n */\nexport const useRoundGauge = (part: string) => {\n  const ctx = useGauge()\n  if (ctx.shape !== \"round\") {\n    throw new Error(`${part} must be rendered inside a round <Gauge>.`)\n  }\n  return ctx\n}\n\n/** The reading, domain and animation every gauge root takes. */\nexport type GaugeReadingProps = {\n  /**\n   * The reading, or a `[from, to]` range to fill between. On an open domain\n   * the two ends are put in order; on a wrapped one the range runs forward\n   * from `from`, so `[22, 6]` on a 24-hour ring is the night, not the day.\n   */\n  value: number | GaugeRange\n  min?: number\n  max?: number\n  /**\n   * Animate towards new values. `true` uses a gentle spring; pass a\n   * `GaugeTransition` to tune it or switch to a timed tween. Omit for\n   * instant updates.\n   */\n  transition?: GaugeTransition | boolean\n  /** Where the value starts on mount when animating, for a sweep-in effect.\n      A single number starts both ends of a range there, so it opens out. */\n  initialValue?: number | GaugeRange\n  /**\n   * Treat the domain as circular, as a compass rose or clock face is: the\n   * value is mapped into the domain modulo its span and animates the\n   * shortest way round, so a heading crossing north never swings the long\n   * way back. Meant for closed rings, where `min` and `max` read the same.\n   */\n  wrap?: boolean\n}\n\n/** The domain, geometry and animation a round gauge takes. */\nexport type GaugeDomainProps = GaugeReadingProps & {\n  /** Gauge degrees, clockwise from six o'clock. */\n  startAngle?: number\n  endAngle?: number\n  /** Reference radius in SVG units. Every child positions itself against this. */\n  radius?: number\n  /**\n   * Turn the whole scale this many degrees clockwise, the way a compass card\n   * turns under a fixed lubber line. Animated by `transition`, always the\n   * shortest way round.\n   */\n  rotate?: number\n}\n\n/**\n * Resolves the domain and follows the value: the half of a gauge's context\n * that is the same whatever its shape.\n */\nconst useGaugeReading = ({\n  value,\n  min = 0,\n  max = 100,\n  transition,\n  initialValue,\n  wrap = false,\n}: GaugeReadingProps): GaugeReading => {\n  const lo = Math.min(min, max)\n  const hi = Math.max(min, max)\n  const span = hi - lo\n  /* A wrapped domain folds any value back inside itself instead of clamping,\n     so 370° on a compass reads as 10°. */\n  const fold = (v: number) => foldValue(v, lo, hi)\n  const settle = wrap ? fold : (v: number) => clamp(v, lo, hi)\n\n  const range = typeof value !== \"number\"\n  /* An open range is put in order so every part can take `from` as the low\n     end; a wrapped one keeps its order, which is what says which way round\n     the ring it runs. */\n  const ends = (pair: GaugeRange) =>\n    wrap\n      ? [settle(pair[0]), settle(pair[1])]\n      : [settle(Math.min(...pair)), settle(Math.max(...pair))]\n  const [targetFrom, target] = range ? ends(value) : [min, settle(value)]\n  const [initialFrom, initialTo] =\n    initialValue === undefined\n      ? [undefined, undefined]\n      : typeof initialValue === \"number\"\n        ? [settle(initialValue), settle(initialValue)]\n        : range\n          ? ends(initialValue)\n          : [undefined, settle(initialValue[1])]\n\n  const shown = useAnimatedValue(target, transition, span, initialTo, wrap)\n  /* Followed on a single value too, so the hooks run the same every render;\n     it just sits at `min` there. */\n  const shownFrom = useAnimatedValue(\n    targetFrom,\n    transition,\n    span,\n    range ? initialFrom : undefined,\n    wrap\n  )\n\n  return {\n    /* Folded when wrapped, otherwise as the spring left it, overshoot and\n       all — a bounce past the end is part of the motion. */\n    value: wrap ? fold(shown) : shown,\n    target,\n    from: range && wrap ? fold(shownFrom) : shownFrom,\n    targetFrom,\n    range,\n    wrap,\n    min,\n    max,\n  }\n}\n\n/**\n * The context a round gauge root publishes to its children. Shared by\n * `Gauge` and `GaugeInset` so a gauge nested inside another animates and\n * maps values exactly like a top-level one, and so the defaults live in one\n * place.\n */\nexport const useGaugeContextValue = ({\n  startAngle = 40,\n  endAngle = 320,\n  radius = 200,\n  rotate = 0,\n  ...domain\n}: GaugeDomainProps): GaugeContextValue & GaugeRoundShape => {\n  const reading = useGaugeReading(domain)\n  /* The rotation is a bearing, so it always takes the short way round. */\n  const turned = useAnimatedValue(\n    rotate,\n    domain.transition,\n    360,\n    undefined,\n    true\n  )\n  const a0 = startAngle + turned\n  const a1 = endAngle + turned\n  const angleOf = (v: number) =>\n    valueToAngle(v, reading.min, reading.max, a0, a1)\n\n  return {\n    ...reading,\n    shape: \"round\",\n    scale: arcScale(radius, a0, a1),\n    positionOf: angleOf,\n    startAngle: a0,\n    endAngle: a1,\n    radius,\n    angleOf,\n  }\n}\n\n/** The domain, geometry and animation a linear gauge takes. */\nexport type LinearGaugeDomainProps = GaugeReadingProps & {\n  /** Which way the bar runs. A vertical bar fills upward. */\n  orientation?: GaugeOrientation\n  /** Length of the bar in SVG units, end to end. Every child positions\n      itself along it, centred on the origin. */\n  length?: number\n}\n\n/** The context a linear gauge root publishes, as `useGaugeContextValue`\n    does for a round one. */\nexport const useLinearGaugeContextValue = ({\n  orientation = \"horizontal\",\n  length = 400,\n  ...domain\n}: LinearGaugeDomainProps): GaugeContextValue & GaugeLinearShape => {\n  const reading = useGaugeReading(domain)\n  const scale = lineScale(orientation, length)\n\n  return {\n    ...reading,\n    shape: \"linear\",\n    scale,\n    positionOf: (v) =>\n      valueToAngle(v, reading.min, reading.max, scale.start, scale.end),\n    orientation,\n    length,\n  }\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/context.tsx"
    },
    {
      "path": "components/gauge/arcs.tsx",
      "content": "\"use client\"\n\nimport { foldValue } from \"./math\"\nimport type { StrokeCap, Zone } from \"./utils\"\n\nimport { useGauge } from \"./context\"\n\ntype ArcStyleProps = {\n  width?: number\n  color?: string\n  opacity?: number\n  cap?: StrokeCap\n  /** Offset from the scale's reference line: radial on a dial, across a\n      bar. Positive moves outward, away from the centre or up from a bar. */\n  offset?: number\n}\n\n/** The whole scale as a background: a dial's full sweep, a bar's length. */\nexport const GaugeTrack = ({\n  width = 24,\n  color = \"currentColor\",\n  opacity = 0.15,\n  cap = \"round\",\n  offset = 0,\n}: ArcStyleProps) => {\n  const { scale } = useGauge()\n  return (\n    <path\n      d={scale.path(scale.start, scale.end, offset)}\n      fill=\"none\"\n      stroke={color}\n      strokeOpacity={opacity}\n      strokeWidth={width}\n      strokeLinecap={cap}\n    />\n  )\n}\n\nexport type GaugeArcProps = ArcStyleProps & {\n  /** Domain value the arc is anchored at. Defaults to `min`, or to the start\n      of the gauge's range. */\n  from?: number\n  /** Domain value the arc ends at. Defaults to the current value. */\n  to?: number\n  /**\n   * Grow the arc back from the end of the sweep instead of forward from its\n   * start. The length is the same; only the end it is anchored to changes,\n   * which is what a dial filling towards its start needs.\n   */\n  reverse?: boolean\n  /**\n   * Carry the arc on round a closed ring instead of clamping it at the\n   * domain: a span from eleven at night to two in the morning crosses the\n   * seam in one piece. The arc runs forward from `from` however far past\n   * `max` the `to` reaches.\n   */\n  wrap?: boolean\n}\n\n/**\n * The filled arc, from `min` (or `from`) up to the current value (or `to`).\n * The ends may arrive either way round: an arc anchored above the value\n * grows back towards it, which is what a dial centred on zero needs. On a\n * gauge showing a range it fills between the two ends, and on a wrapped one\n * runs forward from the start across the seam where the range does.\n */\nexport const GaugeArc = ({\n  width = 24,\n  color = \"currentColor\",\n  opacity = 1,\n  cap = \"round\",\n  offset = 0,\n  from,\n  to,\n  reverse = false,\n  wrap = false,\n}: GaugeArcProps) => {\n  const gauge = useGauge()\n  const { min, max, value, scale, positionOf } = gauge\n  const tail = from ?? gauge.from\n  let head = to ?? value\n  /* A range on a closed ring is read forward from its start, so the far end\n     is carried round past the seam when it sits below the start. */\n  const forward =\n    gauge.range && gauge.wrap && from === undefined && to === undefined\n  if (forward) head = tail + foldValue(head - tail, 0, Math.abs(max - min))\n  const lo = Math.min(tail, head)\n  const hi = Math.max(tail, head)\n\n  let a0: number\n  let a1: number\n  if (wrap || forward) {\n    const span = max - min\n    if (span <= 0) return null\n    /* Positions past the end carry on round a ring, and come back in at\n       the start of a bar. */\n    const sweep = scale.end - scale.start\n    const perUnit = sweep / span\n    a0 = scale.start + ((((lo - min) % span) + span) % span) * perUnit\n    a1 = a0 + Math.min((hi - lo) * perUnit, sweep)\n  } else {\n    ;[a0, a1] = reverse\n      ? [positionOf(max - (hi - lo)), positionOf(max)]\n      : [positionOf(lo), positionOf(hi)]\n  }\n  if (a1 <= a0) return null\n\n  return (\n    <path\n      d={scale.path(a0, a1, offset)}\n      fill=\"none\"\n      stroke={color}\n      strokeOpacity={opacity}\n      strokeWidth={width}\n      strokeLinecap={cap}\n    />\n  )\n}\n\nexport type GaugeStackPart = {\n  /** Share of the fill. Weighted parts split the value between them. */\n  weight?: number\n  /** Fixed upper cutoff in domain units, for parts that stand still while\n      the value sweeps across them. Runs from the end of the part before. */\n  to?: number\n  color: string\n  opacity?: number\n}\n\nexport type GaugeStackProps = Omit<ArcStyleProps, \"color\"> & {\n  parts: GaugeStackPart[]\n  /** Domain value the stack starts at. Defaults to `min`, or to the start of\n      the gauge's range. */\n  from?: number\n}\n\n/**\n * The value arc split into parts that sweep as one. A part with a `weight`\n * takes that share of the fill, so a memory ring keeps wired, app and cache\n * in proportion frame by frame; a part with a fixed `to` runs from the end\n * of the one before and is clipped to the value, so a night of sleep stages\n * draws on in order as the dial sweeps in. Use one kind or the other in a\n * single stack.\n */\nexport const GaugeStack = ({\n  parts,\n  width = 24,\n  opacity = 1,\n  cap = \"butt\",\n  offset = 0,\n  from,\n}: GaugeStackProps) => {\n  const gauge = useGauge()\n  const { value, scale, positionOf } = gauge\n  if (parts.length === 0) return null\n\n  const total = parts.reduce((sum, p) => sum + (p.weight ?? 0), 0)\n  const start = from ?? gauge.from\n  const fill = value - start\n\n  /* Each part runs from the end of the one before: its own cutoff, or its\n     share of the fill. */\n  const ends = parts.reduce<number[]>(\n    (acc, p) => [\n      ...acc,\n      p.to ??\n        (acc.at(-1) ?? start) +\n          (total > 0 ? (fill * (p.weight ?? 0)) / total : 0),\n    ],\n    []\n  )\n\n  return (\n    <g opacity={opacity}>\n      {parts.map((p, i) => {\n        const a0 = positionOf(i === 0 ? start : ends[i - 1])\n        const a1 = positionOf(Math.min(ends[i], value))\n        if (a1 <= a0) return null\n        return (\n          <path\n            key={i}\n            d={scale.path(a0, a1, offset)}\n            fill=\"none\"\n            stroke={p.color}\n            strokeOpacity={p.opacity}\n            strokeWidth={width}\n            strokeLinecap={cap}\n          />\n        )\n      })}\n    </g>\n  )\n}\n\nexport type GaugeZonesProps = Omit<ArcStyleProps, \"color\"> & {\n  /**\n   * Ordered cutoffs. Each zone runs from the previous zone's `to` (or `min`)\n   * up to its own `to`; the last zone always extends to `max`.\n   */\n  zones: Zone[]\n  /** Gap between neighbouring zones, in the scale's units: degrees round a\n      dial, SVG units along a bar. */\n  gap?: number\n  /**\n   * Cap on the two outer ends of the whole band (the start of the first zone\n   * and the end of the last). `cap` covers the joints between zones. Defaults\n   * to `cap`, so a single value still rounds or squares every end.\n   */\n  endCap?: StrokeCap\n}\n\n/** Coloured bands marking the cutoffs of the domain. */\nexport const GaugeZones = ({\n  zones,\n  width = 8,\n  opacity = 1,\n  cap = \"butt\",\n  endCap = cap,\n  offset = 0,\n  gap = 0,\n}: GaugeZonesProps) => {\n  const { min, max, scale, positionOf } = useGauge()\n  if (zones.length === 0) return null\n\n  /* Segments are drawn with butt caps and rounded where asked with a disc of\n     the stroke width, which is exactly the shape a round cap adds. Drawing\n     the discs separately lets the joints and the outer ends differ. */\n  const dot = (position: number, color: string, key: string) => {\n    const { x, y } = scale.point(position, offset)\n    return <circle key={key} cx={x} cy={y} r={width / 2} fill={color} />\n  }\n\n  /* Each zone runs from the previous cutoff to its own, clamped to the domain. */\n  const segments = zones.reduce<{ from: number; to: number }[]>(\n    (acc, zone, i) => {\n      const from = i === 0 ? min : acc[i - 1].to\n      const isLast = i === zones.length - 1\n      const to = isLast ? max : Math.min(Math.max(zone.to, from), max)\n      acc.push({ from, to })\n      return acc\n    },\n    []\n  )\n\n  return (\n    <g opacity={opacity}>\n      {zones.map((zone, i) => {\n        const isFirst = i === 0\n        const isLast = i === zones.length - 1\n        const { from, to } = segments[i]\n        const a0 = positionOf(from) + (isFirst ? 0 : gap / 2)\n        const a1 = positionOf(to) - (isLast ? 0 : gap / 2)\n        if (a1 <= a0) return null\n        const startCap = isFirst ? endCap : cap\n        const finishCap = isLast ? endCap : cap\n        return (\n          <g key={i}>\n            <path\n              d={scale.path(a0, a1, offset)}\n              fill=\"none\"\n              stroke={zone.color}\n              strokeWidth={width}\n              strokeLinecap=\"butt\"\n            />\n            {startCap === \"round\" && dot(a0, zone.color, \"start\")}\n            {finishCap === \"round\" && dot(a1, zone.color, \"end\")}\n          </g>\n        )\n      })}\n    </g>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/arcs.tsx"
    },
    {
      "path": "components/gauge/ticks.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { cutValues, foldValue, stepValues } from \"./math\"\nimport type { GaugeContextValue } from \"./context\"\nimport {\n  fadeColor,\n  fontClass,\n  weightClass,\n  type FontFamily,\n  type FontWeight,\n  type StrokeCap,\n  type TextAnchor,\n  type ValueColor,\n} from \"./utils\"\n\nimport { useGauge } from \"./context\"\n\n/**\n * Evenly spaced domain values for ticks. On a closed ring the last step lands\n * on the first, so it is dropped rather than drawn twice.\n */\nconst tickValues = ({ min, max, scale }: GaugeContextValue, count: number) => {\n  const values = stepValues(min, max, count)\n  if (scale.closed) values.pop()\n  return values\n}\n\ntype CrossLineProps = {\n  /** Length of each line across the scale: along the radius of a dial. */\n  length?: number\n  width?: number\n  /** One colour, or a function of each line's value to colour some apart:\n      the ticks inside a range, or each in the colour of its zone. */\n  color?: ValueColor\n  opacity?: number\n  cap?: StrokeCap\n  /** Offset of the line's midpoint from the scale's reference line. */\n  offset?: number\n}\n\n/** Lines standing across the scale at domain values, as ticks do. */\nconst CrossLines = ({\n  values,\n  length = 12,\n  lengthOf,\n  width = 2,\n  color = \"currentColor\",\n  opacity = 1,\n  cap = \"round\",\n  offset = 0,\n}: CrossLineProps & {\n  values: number[]\n  /** A length per line, in place of `length`. */\n  lengthOf?: (value: number) => number\n}) => {\n  const { scale, positionOf } = useGauge()\n  const each = typeof color === \"function\" ? color : undefined\n\n  return (\n    <g\n      stroke={typeof color === \"string\" ? color : undefined}\n      strokeOpacity={opacity}\n      strokeWidth={width}\n      strokeLinecap={cap}\n    >\n      {values.map((value, i) => {\n        const half = (lengthOf?.(value) ?? length) / 2\n        const a = scale.point(positionOf(value), offset - half)\n        const b = scale.point(positionOf(value), offset + half)\n        return (\n          <line\n            key={i}\n            x1={a.x}\n            y1={a.y}\n            x2={b.x}\n            y2={b.y}\n            stroke={each?.(value)}\n          />\n        )\n      })}\n    </g>\n  )\n}\n\nexport type GaugeTicksProps = CrossLineProps & {\n  /** Number of intervals across the domain; renders `count + 1` ticks. */\n  count?: number\n}\n\n/** Evenly spaced tick marks along the whole scale. */\nexport const GaugeTicks = ({ count = 10, ...props }: GaugeTicksProps) => {\n  const gauge = useGauge()\n  return <CrossLines values={tickValues(gauge, count)} {...props} />\n}\n\nexport type GaugeTickFillProps = Omit<CrossLineProps, \"color\"> & {\n  /** Number of intervals across the domain; renders `count + 1` ticks. */\n  count?: number\n  /** Where the lit stretch starts. Defaults to the start of the gauge's\n      range, or `min` on a single value. */\n  from?: number\n  /** Where it ends. Defaults to the current value, or a range's far end. */\n  to?: number\n  /**\n   * `fill` lights every tick from `from` to `to`, as an arc would fill.\n   * `nearest` lights only the tick nearest each end: on a single value a\n   * pointer made of ticks, on a range one at either end.\n   */\n  mode?: \"fill\" | \"nearest\"\n  /** Colour of the lit ticks: one, or a function of each tick's value, so a\n      fill can run through zones or a gradient. */\n  color?: ValueColor\n  /** Colour of the ticks left unlit. Defaults to a faint `currentColor`. */\n  dimColor?: ValueColor\n  /** Length of a lit tick, for meters whose live segments stand out.\n      Defaults to `length`. */\n  litLength?: number\n}\n\n/**\n * Ticks that are the fill: the whole sweep is ticked, and those between the\n * ends of the value are lit while the rest stay dim. It follows the value\n * frame by frame as `GaugeArc` does, fills a range between its ends, and on\n * a wrapped gauge runs forward from the start across the seam. With\n * `mode=\"nearest\"` it lights a single tick at each end instead, for a\n * pointer drawn in ticks. Lit and dim colours can each be a function of the\n * tick's value, so one primitive covers segmented meters, LED rings, zoned\n * dials and dot-matrix pointers.\n */\nexport const GaugeTickFill = ({\n  count = 40,\n  from,\n  to,\n  mode = \"fill\",\n  color = \"currentColor\",\n  dimColor = fadeColor(\"currentColor\", 15),\n  length = 12,\n  litLength = length,\n  ...props\n}: GaugeTickFillProps) => {\n  const gauge = useGauge()\n  const values = tickValues(gauge, count)\n  const tail = from ?? gauge.from\n  const head = to ?? gauge.value\n  const span = Math.abs(gauge.max - gauge.min)\n  /* Slack for ticks that land on an end, whose step values carry float\n     error. */\n  const eps = span * 1e-6\n\n  /* How far round from the start a value sits, on a closed ring. */\n  const ahead = (v: number) => foldValue(v - tail + eps, 0, span)\n  const between = (v: number) =>\n    gauge.wrap\n      ? ahead(v) <= ahead(head) + eps\n      : v >= Math.min(tail, head) - eps && v <= Math.max(tail, head) + eps\n\n  /* The tick nearest a value, measured round the seam on a ring. */\n  const nearestTo = (target: number) => {\n    const gap = (v: number) => {\n      const d = Math.abs(v - target)\n      return gauge.wrap ? Math.min(d, span - d) : d\n    }\n    return values.reduce((best, v) => (gap(v) < gap(best) ? v : best))\n  }\n  const ends =\n    mode === \"nearest\"\n      ? new Set(\n          gauge.range || from !== undefined\n            ? [nearestTo(tail), nearestTo(head)]\n            : [nearestTo(head)]\n        )\n      : null\n\n  const lit = new Map(\n    values.map((v) => [v, ends ? ends.has(v) : between(v)] as const)\n  )\n  const paint = (c: ValueColor, v: number) =>\n    typeof c === \"function\" ? c(v) : c\n\n  return (\n    <CrossLines\n      values={values}\n      length={length}\n      lengthOf={(v) => (lit.get(v) ? litLength : length)}\n      color={(v) => paint(lit.get(v) ? color : dimColor, v)}\n      {...props}\n    />\n  )\n}\n\nexport type GaugeMarksProps = CrossLineProps & {\n  /** Domain values to mark, for example the cutoffs between zones. */\n  values?: number[]\n  /**\n   * Cut the domain into this many equal segments instead: the marks fall\n   * between the segments, none at the two ends. On a closed ring the seam is\n   * cut too, since the last segment meets the first there. Drawn in the\n   * surface's colour over a band, they slice it the way a water ring is cut\n   * into glasses.\n   */\n  count?: number\n}\n\n/** Marks across the scale at arbitrary domain values, or at equal cuts of\n    the domain. */\nexport const GaugeMarks = ({ values, count, ...props }: GaugeMarksProps) => {\n  const { min, max, scale } = useGauge()\n  const marks =\n    values ??\n    (count && count > 1\n      ? [...(scale.closed ? [min] : []), ...cutValues(min, max, count)]\n      : [])\n  return <CrossLines values={marks} {...props} />\n}\n\nexport type GaugeTickLabelsProps = {\n  count?: number\n  /** Offset of the label from the scale's reference line. */\n  offset?: number\n  /** Which edge of each label sits at its point: `end` lines labels up\n      against the left of a vertical bar. */\n  anchor?: TextAnchor\n  fontSize?: number\n  /** One colour, or a function of each label's value. */\n  color?: ValueColor\n  font?: FontFamily\n  weight?: FontWeight\n  decimals?: number\n  format?: (value: number) => string\n}\n\n/** Numeric labels at evenly spaced values along the scale. */\nexport const GaugeTickLabels = ({\n  count = 10,\n  offset = -48,\n  anchor = \"middle\",\n  fontSize = 16,\n  color = \"currentColor\",\n  font = \"sans\",\n  weight = \"medium\",\n  decimals = 0,\n  format,\n}: GaugeTickLabelsProps) => {\n  const gauge = useGauge()\n  const { scale, positionOf } = gauge\n  const values = tickValues(gauge, count)\n  const each = typeof color === \"function\" ? color : undefined\n\n  return (\n    <g\n      fill={typeof color === \"string\" ? color : undefined}\n      fontSize={fontSize}\n      textAnchor={anchor}\n      dominantBaseline=\"central\"\n      className={cn(fontClass[font], weightClass[weight], \"tabular-nums\")}\n    >\n      {values.map((value, i) => {\n        const p = scale.point(positionOf(value), offset)\n        return (\n          <text key={i} x={p.x} y={p.y} fill={each?.(value)}>\n            {format ? format(value) : value.toFixed(decimals)}\n          </text>\n        )\n      })}\n    </g>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/ticks.tsx"
    },
    {
      "path": "components/gauge/text.tsx",
      "content": "\"use client\"\n\nimport type { ReactNode } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  fontClass,\n  weightClass,\n  type FontFamily,\n  type FontWeight,\n  type GaugeEnd,\n  type TextAnchor,\n} from \"./utils\"\n\nimport { useGauge } from \"./context\"\n\nexport type GaugeTextProps = {\n  /** Position relative to the gauge centre in SVG units. Positive y is down. */\n  x?: number\n  y?: number\n  fontSize?: number\n  color?: string\n  font?: FontFamily\n  weight?: FontWeight\n  anchor?: TextAnchor\n  className?: string\n  children: ReactNode\n}\n\n/** A free-standing label anywhere on the gauge. */\nexport const GaugeText = ({\n  x = 0,\n  y = 0,\n  fontSize = 24,\n  color = \"currentColor\",\n  font = \"sans\",\n  weight = \"medium\",\n  anchor = \"middle\",\n  className,\n  children,\n}: GaugeTextProps) => (\n  <text\n    x={x}\n    y={y}\n    fill={color}\n    fontSize={fontSize}\n    textAnchor={anchor}\n    dominantBaseline=\"central\"\n    className={cn(fontClass[font], weightClass[weight], className)}\n  >\n    {children}\n  </text>\n)\n\nexport type GaugeValueProps = Omit<GaugeTextProps, \"children\"> & {\n  decimals?: number\n  /** Formats each number shown; a range formats its two ends apart. */\n  format?: (value: number) => string\n  /** Placed between the two ends of a range. */\n  separator?: string\n  /** Show only one end of a range rather than both. */\n  end?: GaugeEnd\n}\n\n/** The current value as text, or a range as its two ends. */\nexport const GaugeValue = ({\n  decimals = 0,\n  format,\n  separator = \"–\",\n  end,\n  className,\n  fontSize = 96,\n  weight = \"semibold\",\n  ...props\n}: GaugeValueProps) => {\n  const { value, from, range } = useGauge()\n  const text = (v: number) => (format ? format(v) : v.toFixed(decimals))\n  return (\n    <GaugeText\n      fontSize={fontSize}\n      weight={weight}\n      className={cn(\"tabular-nums\", className)}\n      {...props}\n    >\n      {end === \"from\"\n        ? text(from)\n        : range && !end\n          ? `${text(from)}${separator}${text(value)}`\n          : text(value)}\n    </GaugeText>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/text.tsx"
    },
    {
      "path": "components/gauge/needle.tsx",
      "content": "\"use client\"\n\nimport { useId } from \"react\"\n\nimport { polar, valueToTurnedAngle } from \"./math\"\nimport type { GaugeEnd, NeedleStyle } from \"./utils\"\n\nimport { useRoundGauge } from \"./context\"\n\nexport type GaugeNeedleProps = {\n  /**\n   * `line` is a stroke with round ends, `pointer` a tapered blade that is\n   * widest at the centre and ends in a rounded tip, and `compass` a lozenge\n   * whose two halves can take different colours. `arrow` is a shaft ending in\n   * a triangular arrowhead, sized from the width, for wind or flow direction.\n   */\n  style?: NeedleStyle\n  /** Length as a fraction of the reference radius. */\n  length?: number\n  /** Stroke width for `line`; width at the centre for the filled styles. */\n  width?: number\n  color?: string\n  /** How far the needle extends behind the centre, in SVG units. */\n  tail?: number\n  /** Colour of the tail half of a `pointer`, `compass` or `arrow`. Defaults to `color`. */\n  tailColor?: string\n  /**\n   * Radius of a disc drawn at the end of the tail in `tailColor`, like the\n   * counterweight on a wind vane. 0 leaves the tail plain.\n   */\n  tailDot?: number\n  /**\n   * Sweeps of the arc the needle makes across the domain. A clock's minute\n   * hand turns 12 times while the hour hand turns once.\n   */\n  turns?: number\n  /**\n   * Radius of a clear circle around the centre, in SVG units, that splits the\n   * needle in two so a readout can sit between the halves. 0 draws it whole.\n   */\n  gap?: number\n  /** Which end of a range the needle points at. */\n  end?: GaugeEnd\n}\n\nconst pts = (points: { x: number; y: number }[]) =>\n  points.map((p) => `${p.x},${p.y}`).join(\" \")\n\n/**\n * A needle pointing at the current value. Add a `GaugeHub` for the pivot, or\n * a `gap` to clear the centre for a readout instead.\n */\nexport const GaugeNeedle = ({ gap = 0, ...props }: GaugeNeedleProps) => {\n  /* useId's output is not a safe fragment identifier, so keep word\n     characters only. */\n  const maskId = `gauge-needle-gap${useId().replace(/\\W/g, \"\")}`\n  if (gap <= 0) return <NeedleShape {...props} />\n\n  /* A mask rather than geometry, so every style splits the same way. The\n     cut edges follow the circle, which on a gap this much wider than the\n     needle reads as straight. */\n  return (\n    <g>\n      <mask\n        id={maskId}\n        maskUnits=\"userSpaceOnUse\"\n        x={-1e4}\n        y={-1e4}\n        width={2e4}\n        height={2e4}\n      >\n        <rect x={-1e4} y={-1e4} width={2e4} height={2e4} fill=\"white\" />\n        <circle r={gap} fill=\"black\" />\n      </mask>\n      <g mask={`url(#${maskId})`}>\n        <NeedleShape {...props} />\n      </g>\n    </g>\n  )\n}\n\nconst NeedleShape = ({\n  style = \"line\",\n  length = 0.85,\n  width = 6,\n  color = \"currentColor\",\n  tail = 30,\n  tailColor = color,\n  tailDot = 0,\n  turns = 1,\n  end = \"to\",\n}: Omit<GaugeNeedleProps, \"gap\">) => {\n  const gauge = useRoundGauge(\"GaugeNeedle\")\n  const { radius, min, max, startAngle, endAngle } = gauge\n  const value = end === \"from\" ? gauge.from : gauge.value\n\n  const angle = valueToTurnedAngle(value, min, max, startAngle, endAngle, turns)\n\n  const tip = polar(radius * length, angle)\n  const back = polar(-tail, angle)\n  const endDot =\n    tail > 0 && tailDot > 0 ? (\n      <circle cx={back.x} cy={back.y} r={tailDot} fill={tailColor} />\n    ) : null\n\n  if (style === \"arrow\") {\n    /* A shaft of `width` capped by a triangular head that scales with it,\n       never longer than the needle itself. The shaft stops where the head\n       begins so the two never overlap, and the tail is the shaft carried on\n       behind the centre in its own colour, rounded off at the end. */\n    const reach = radius * length\n    const headLength = Math.min(width * 4, reach)\n    const base = polar(reach - headLength, angle)\n    const shaftLeft = polar(width / 2, angle - 90)\n    const shaftRight = polar(width / 2, angle + 90)\n    const headLeft = polar(width * 2, angle - 90)\n    const headRight = polar(width * 2, angle + 90)\n    const offset = (\n      p: { x: number; y: number },\n      d: { x: number; y: number }\n    ) => ({\n      x: p.x + d.x,\n      y: p.y + d.y,\n    })\n\n    return (\n      <g>\n        {tail > 0 && (\n          <>\n            <polygon\n              points={pts([\n                shaftLeft,\n                shaftRight,\n                offset(back, shaftRight),\n                offset(back, shaftLeft),\n              ])}\n              fill={tailColor}\n            />\n            <circle cx={back.x} cy={back.y} r={width / 2} fill={tailColor} />\n          </>\n        )}\n        <polygon\n          points={pts([\n            shaftLeft,\n            offset(base, shaftLeft),\n            offset(base, headLeft),\n            tip,\n            offset(base, headRight),\n            offset(base, shaftRight),\n            shaftRight,\n          ])}\n          fill={color}\n        />\n        {endDot}\n      </g>\n    )\n  }\n\n  if (style === \"line\") {\n    return (\n      <>\n        <line\n          x1={back.x}\n          y1={back.y}\n          x2={tip.x}\n          y2={tip.y}\n          stroke={color}\n          strokeWidth={width}\n          strokeLinecap=\"round\"\n        />\n        {endDot}\n      </>\n    )\n  }\n\n  /* The filled styles are two polygons meeting at the centre, so the tail can\n     take its own colour. Their shared base runs across the pivot. Both ends\n     keep a fifth of the width and are rounded off with a disc of that size,\n     as a round cap would, so a thin needle never vanishes to a point. */\n  const left = polar(width / 2, angle - 90)\n  const right = polar(width / 2, angle + 90)\n  const endRadius = width * 0.2\n  const endLeft = polar(endRadius, angle - 90)\n  const endRight = polar(endRadius, angle + 90)\n  const blade = (end: { x: number; y: number }) =>\n    pts([\n      { x: end.x + endLeft.x, y: end.y + endLeft.y },\n      { x: end.x + endRight.x, y: end.y + endRight.y },\n      right,\n      left,\n    ])\n\n  /* A pointer's tail is a blunt stub; a compass tapers the same both ways. */\n  const rear =\n    style === \"compass\"\n      ? blade(back)\n      : pts([\n          { x: back.x + left.x * 0.6, y: back.y + left.y * 0.6 },\n          { x: back.x + right.x * 0.6, y: back.y + right.y * 0.6 },\n          right,\n          left,\n        ])\n\n  return (\n    <g>\n      {tail > 0 && <polygon points={rear} fill={tailColor} />}\n      {tail > 0 && style === \"compass\" && (\n        <circle cx={back.x} cy={back.y} r={endRadius} fill={tailColor} />\n      )}\n      <polygon points={blade(tip)} fill={color} />\n      <circle cx={tip.x} cy={tip.y} r={endRadius} fill={color} />\n      {endDot}\n    </g>\n  )\n}\n\nexport type GaugeHubProps = {\n  radius?: number\n  color?: string\n}\n\n/** The pivot disc at the centre, drawn once however many needles there are. */\nexport const GaugeHub = ({\n  radius = 12,\n  color = \"currentColor\",\n}: GaugeHubProps) => (radius > 0 ? <circle r={radius} fill={color} /> : null)\n",
      "type": "registry:ui",
      "target": "@components/gauge/needle.tsx"
    },
    {
      "path": "components/gauge/dot.tsx",
      "content": "\"use client\"\n\nimport { valueToTurnedAngle } from \"./math\"\nimport type { GaugeEnd } from \"./utils\"\n\nimport { useGauge } from \"./context\"\n\nexport type GaugeDotProps = {\n  /** Radius of the dot itself, in SVG units. */\n  radius?: number\n  color?: string\n  opacity?: number\n  /** Offset of the dot's centre from the scale's reference line. */\n  offset?: number\n  /**\n   * Sweeps of the scale the dot makes across the domain, as\n   * `GaugeNeedle.turns`: two sends it round twice while the value crosses\n   * the domain once.\n   */\n  turns?: number\n  /**\n   * Width of a ring drawn behind the dot in `haloColor`, so it stands off\n   * whatever band it rides. 0 leaves the dot bare.\n   */\n  halo?: number\n  /** Colour of the halo: the surface under the gauge, usually. */\n  haloColor?: string\n  /** Which end of a range the dot rides. Two dots, one at each end, make\n      the handles of a range. */\n  end?: GaugeEnd\n}\n\n/**\n * A disc riding the scale at the current value: the head of a progress ring,\n * a body on its orbit, the handle on a dial or a slider. `GaugeMarks` stands at fixed values\n * and points along the radius; this follows the value the way a needle does,\n * and is round at any size a stroke cap could not reach.\n */\nexport const GaugeDot = ({\n  radius = 10,\n  color = \"currentColor\",\n  opacity = 1,\n  offset = 0,\n  turns = 1,\n  halo = 0,\n  haloColor = \"var(--background)\",\n  end = \"to\",\n}: GaugeDotProps) => {\n  const gauge = useGauge()\n  const position = valueToTurnedAngle(\n    end === \"from\" ? gauge.from : gauge.value,\n    gauge.min,\n    gauge.max,\n    gauge.scale.start,\n    gauge.scale.end,\n    turns\n  )\n  const { x, y } = gauge.scale.point(position, offset)\n\n  return (\n    <>\n      {halo > 0 && (\n        <circle\n          cx={x}\n          cy={y}\n          r={radius + halo}\n          fill={haloColor}\n          fillOpacity={opacity}\n        />\n      )}\n      <circle cx={x} cy={y} r={radius} fill={color} fillOpacity={opacity} />\n    </>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/dot.tsx"
    },
    {
      "path": "components/gauge/tooltip.tsx",
      "content": "\"use client\"\n\nimport {\n  useEffect,\n  useEffectEvent,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type CSSProperties,\n  type ReactNode,\n} from \"react\"\nimport { createPortal } from \"react-dom\"\n\nimport { cn } from \"@/lib/utils\"\nimport { foldValue } from \"./math\"\n\nimport { useGauge } from \"./context\"\n\n/**\n * What the tooltip is pinned to. `pointer` follows the cursor round the\n * gauge, `value` sits on the scale at the value, and the rest are fixed\n * points of the gauge: its centre, or the middle of an edge or a corner of\n * the box its scale fills.\n */\nexport type GaugeTooltipPosition =\n  | \"pointer\"\n  | \"value\"\n  | \"center\"\n  | \"top\"\n  | \"right\"\n  | \"bottom\"\n  | \"left\"\n  | \"top-left\"\n  | \"top-right\"\n  | \"bottom-left\"\n  | \"bottom-right\"\n\n/**\n * Which way the bubble stands off its point. `auto` picks for the position:\n * outward from the scale at the value, so the bubble swings round a dial with it,\n * above the pointer, centred on the centre, and away from the dial at an\n * edge or corner.\n */\nexport type GaugeTooltipSide =\n  \"auto\" | \"center\" | \"top\" | \"right\" | \"bottom\" | \"left\"\n\nexport type GaugeTooltipProps = {\n  position?: GaugeTooltipPosition\n  side?: GaugeTooltipSide\n  /**\n   * Domain value a `value` tooltip points at and shows. Defaults to the\n   * current value, or for a range the middle of it, showing both ends; a\n   * fixed one marks a target or a cutoff.\n   */\n  at?: number\n  /**\n   * How far out from the scale's reference line the point sits, in SVG\n   * units: the spot on the scale for `value`, and the box round the scale\n   * for the edges and corners.\n   */\n  offset?: number\n  /** Room between the point and the bubble, in CSS pixels. */\n  gap?: number\n  /**\n   * Keep the bubble on screen: flip it to the far side of its point when\n   * that side has more room, then slide it sideways in from the left or\n   * right edge while it still overlaps its point. On by default.\n   */\n  avoidCollisions?: boolean\n  /** Room kept clear between the bubble and the viewport edge, in CSS pixels. */\n  collisionPadding?: number\n  /**\n   * Hold the tooltip open, or shut, instead of showing it on hover, while\n   * a press that started on the gauge is held, and on keyboard focus.\n   */\n  open?: boolean\n  /** Small line above the value. Leave out for the value on its own. */\n  label?: ReactNode\n  /** Printed after the value, a size down. */\n  unit?: ReactNode\n  decimals?: number\n  /** Formats each number shown; a range formats its two ends apart. */\n  format?: (value: number) => string\n  /** Placed between the two ends of a range. */\n  separator?: string\n  /** Classes for the bubble, merged over its defaults. */\n  className?: string\n  style?: CSSProperties\n  /**\n   * Replaces the default label, value and unit with any HTML, or a function\n   * of the value being shown for content that has to follow it. On a range\n   * the function gets its start as well.\n   */\n  children?: ReactNode | ((value: number, from?: number) => ReactNode)\n}\n\ntype Point = { x: number; y: number }\ntype Size = { width: number; height: number }\ntype Axis = \"x\" | \"y\"\n\nconst extent = { x: \"width\", y: \"height\" } as const\n\n/**\n * The bubble's top left corner for a direction off the point: centred on the\n * point, moved `gap` along the direction, then pushed half its own size the\n * same way, so a side tooltip touches the point with that edge.\n */\nconst corner = (p: Point, d: Point, size: Size, gap: number): Point => {\n  const len = Math.hypot(d.x, d.y) || 1\n  return {\n    x: p.x + (d.x / len) * gap + ((d.x - 1) / 2) * size.width,\n    y: p.y + (d.y / len) * gap + ((d.y - 1) / 2) * size.height,\n  }\n}\n\n/** Directions in screen space, y down, scaled so the longer axis is 1. */\nconst sides: Record<Exclude<GaugeTooltipSide, \"auto\">, Point> = {\n  center: { x: 0, y: 0 },\n  top: { x: 0, y: -1 },\n  right: { x: 1, y: 0 },\n  bottom: { x: 0, y: 1 },\n  left: { x: -1, y: 0 },\n}\n\n/** Where each fixed position sits in the sweep's box, 0 to 1 across. */\nconst boxPoints: Record<string, Point> = {\n  top: { x: 0.5, y: 0 },\n  right: { x: 1, y: 0.5 },\n  bottom: { x: 0.5, y: 1 },\n  left: { x: 0, y: 0.5 },\n  \"top-left\": { x: 0, y: 0 },\n  \"top-right\": { x: 1, y: 0 },\n  \"bottom-left\": { x: 0, y: 1 },\n  \"bottom-right\": { x: 1, y: 1 },\n}\n\n/** What can take keyboard focus, to find the element that owns the gauge's.\n    A range control is not focusable itself but holds the thumbs that are. */\nconst FOCUSABLE =\n  '[tabindex]:not([tabindex=\"-1\"]), a[href], button, input, select, textarea, [data-slot=\"gauge-control\"]'\n\n/* The portal needs a document, which the server render has none of. */\nconst subscribe = () => () => {}\nconst useIsClient = () =>\n  useSyncExternalStore(\n    subscribe,\n    () => true,\n    () => false\n  )\n\n/**\n * An HTML tooltip for the gauge, shown while the pointer is over it. It reads\n * the value with an optional label and unit by default, or renders whatever\n * its children are, and can be pinned to the value on the arc, follow the\n * pointer, or hold a fixed spot on the dial.\n *\n * The bubble is portalled to the body and placed through the SVG's screen\n * transform, so it is never clipped by the gauge and keeps its own CSS size\n * however the gauge, or an inset it sits in, is scaled.\n */\nexport const GaugeTooltip = ({\n  position = \"value\",\n  side = \"auto\",\n  at,\n  offset = 24,\n  gap = 8,\n  avoidCollisions = true,\n  collisionPadding = 8,\n  open,\n  label,\n  unit,\n  decimals = 0,\n  format,\n  separator = \"–\",\n  className,\n  style,\n  children,\n}: GaugeTooltipProps) => {\n  const gauge = useGauge()\n  const isClient = useIsClient()\n  const anchor = useRef<SVGGElement>(null)\n  const box = useRef<HTMLDivElement>(null)\n  const pointer = useRef<Point | null>(null)\n  const [hovered, setHovered] = useState(false)\n  /* A press that starts on the gauge, held until it is let go anywhere. */\n  const [pressed, setPressed] = useState(false)\n  const [focused, setFocused] = useState(false)\n  const shown = open ?? (hovered || pressed || focused)\n\n  const value = at ?? gauge.value\n  /* A range is pointed at in its middle, measured forward round a ring. */\n  const spread = at === undefined && gauge.range\n  const middle = !spread\n    ? value\n    : gauge.wrap\n      ? foldValue(\n          gauge.from +\n            foldValue(gauge.value - gauge.from, 0, gauge.max - gauge.min) / 2,\n          gauge.min,\n          gauge.max\n        )\n      : (gauge.from + gauge.value) / 2\n  /* Where the value sits on the scale. */\n  const along = gauge.positionOf(middle)\n\n  /** The point the bubble is pinned to, in client pixels. */\n  const pinned = (): Point | null => {\n    if (position === \"pointer\") return pointer.current\n    const ctm = anchor.current?.getScreenCTM()\n    if (!ctm) return null\n    let p: Point = { x: 0, y: 0 }\n    if (position === \"value\") p = gauge.scale.point(along, offset)\n    else if (position !== \"center\") {\n      const b = gauge.scale.box(offset)\n      const spot = boxPoints[position]\n      p = { x: b.x + b.width * spot.x, y: b.y + b.height * spot.y }\n    }\n    return new DOMPoint(p.x, p.y).matrixTransform(ctm)\n  }\n\n  /** The way the bubble stands off its point. */\n  const direction = (): Point => {\n    if (side !== \"auto\") return sides[side]\n    if (position === \"pointer\") return sides.top\n    if (position === \"center\") return sides.center\n    if (position !== \"value\") {\n      const spot = boxPoints[position]\n      return { x: spot.x * 2 - 1, y: spot.y * 2 - 1 }\n    }\n    /* Outward from the scale, stretched onto the square so the bubble's\n       nearest edge or corner meets the point at any angle. */\n    const r = gauge.scale.normal(along)\n    const m = Math.max(Math.abs(r.x), Math.abs(r.y)) || 1\n    return { x: r.x / m, y: r.y / m }\n  }\n\n  /* Written straight to the element rather than through state, so a bubble\n     following the pointer moves without a render per pixel. */\n  const place = useEffectEvent(() => {\n    const el = box.current\n    if (!el) return\n    const p = pinned()\n    if (!p) {\n      el.style.visibility = \"hidden\"\n      return\n    }\n    const size = { width: el.offsetWidth, height: el.offsetHeight }\n    let d = direction()\n    let at = corner(p, d, size, gap)\n\n    if (avoidCollisions) {\n      const view = {\n        width: document.documentElement.clientWidth,\n        height: document.documentElement.clientHeight,\n      }\n      const pad = collisionPadding\n      /* How much of the bubble falls outside the padded viewport along one\n         axis. Capped at its size, so a point far off screen reads the same\n         on either side and does not flip back and forth as it scrolls. */\n      const spill = (at: Point, axis: Axis) => {\n        const span = size[extent[axis]]\n        const room = view[extent[axis]] - pad\n        const out =\n          Math.max(0, pad - at[axis]) + Math.max(0, at[axis] + span - room)\n        return Math.min(span, out)\n      }\n      /* Flip to the other side of the point, axis by axis, when the bubble\n         spills over and the far side spills less. */\n      for (const axis of [\"x\", \"y\"] as const) {\n        const before = spill(at, axis)\n        if (before === 0 || d[axis] === 0) continue\n        const flipped = { ...d, [axis]: -d[axis] }\n        const there = corner(p, flipped, size, gap)\n        if (spill(there, axis) < before) {\n          d = flipped\n          at = there\n        }\n      }\n      /* Then slide sideways back inside the left or right edge, but only\n         when the bubble spans its point across, and only as far as it keeps\n         spanning it, so it never leaves its point. Never up or down: that is\n         the way the page scrolls, and a bubble sliding along the top or\n         bottom of the window would creep against its point as it went. */\n      const { width } = size\n      if (p.x >= at.x && p.x <= at.x + width) {\n        const inside = Math.max(pad, Math.min(at.x, view.width - pad - width))\n        at = { ...at, x: Math.max(p.x - width, Math.min(inside, p.x)) }\n      }\n    }\n\n    /* In page coordinates, so the bubble rides a page scroll natively\n       instead of trailing a frame behind it the way a fixed one would. */\n    el.style.visibility = \"\"\n    el.style.left = `${at.x + window.scrollX}px`\n    el.style.top = `${at.y + window.scrollY}px`\n  })\n\n  /* Hover is taken over the whole gauge, from the SVG the anchor sits in. */\n  useEffect(() => {\n    const svg = anchor.current?.ownerSVGElement\n    if (!svg) return\n    const track = (e: PointerEvent) => {\n      pointer.current = { x: e.clientX, y: e.clientY }\n    }\n    const enter = (e: PointerEvent) => {\n      track(e)\n      setHovered(true)\n    }\n    const move = (e: PointerEvent) => {\n      track(e)\n      place()\n    }\n    const leave = () => setHovered(false)\n    const down = (e: PointerEvent) => {\n      track(e)\n      setPressed(true)\n    }\n    svg.addEventListener(\"pointerenter\", enter)\n    svg.addEventListener(\"pointermove\", move)\n    svg.addEventListener(\"pointerleave\", leave)\n    svg.addEventListener(\"pointerdown\", down)\n    return () => {\n      svg.removeEventListener(\"pointerenter\", enter)\n      svg.removeEventListener(\"pointermove\", move)\n      svg.removeEventListener(\"pointerleave\", leave)\n      svg.removeEventListener(\"pointerdown\", down)\n    }\n  }, [])\n\n  /* Keyboard focus on the gauge, or on the nearest focusable element round\n     it such as a GaugeControl, brings the tooltip up too. Only focus that\n     shows a ring counts, so clicking a control does not; a key pressed\n     after the click does, as that is when the browser starts showing it. */\n  useEffect(() => {\n    const owner = anchor.current?.ownerSVGElement?.closest(FOCUSABLE)\n    if (!owner) return\n    /* Focus on the owner or anything in it, such as a range's thumbs. */\n    const within = () => owner.contains(document.activeElement)\n    const check = () =>\n      setFocused(\n        within() &&\n          /* Not a range thumb a GaugeControl focused for a drag. */\n          Boolean(\n            document.activeElement?.matches(\n              \":focus-visible:not([data-pointer])\"\n            )\n          )\n      )\n    /* A key pressed while it has focus is keyboard use, whether or not the\n       browser has switched its ring on yet. */\n    const key = () => setFocused(within())\n    /* Moving between two thumbs leaves one only to land on the other. */\n    const blur = (e: Event) => {\n      const next = (e as FocusEvent).relatedTarget as Node | null\n      if (!owner.contains(next)) setFocused(false)\n    }\n    check()\n    owner.addEventListener(\"focusin\", check)\n    owner.addEventListener(\"keydown\", key)\n    owner.addEventListener(\"focusout\", blur)\n    return () => {\n      owner.removeEventListener(\"focusin\", check)\n      owner.removeEventListener(\"keydown\", key)\n      owner.removeEventListener(\"focusout\", blur)\n    }\n  }, [])\n\n  /* A drag holds the tooltip up until it ends. A GaugeControl captures the\n     pointer on its wrapper, which counts as leaving the SVG and stops its\n     moves reaching it, so the press is followed on the window instead. It\n     also brings the tooltip up on touch, which has no hover. */\n  useEffect(() => {\n    if (!pressed) return\n    const move = (e: PointerEvent) => {\n      pointer.current = { x: e.clientX, y: e.clientY }\n      place()\n    }\n    const release = () => setPressed(false)\n    window.addEventListener(\"pointermove\", move)\n    window.addEventListener(\"pointerup\", release)\n    window.addEventListener(\"pointercancel\", release)\n    return () => {\n      window.removeEventListener(\"pointermove\", move)\n      window.removeEventListener(\"pointerup\", release)\n      window.removeEventListener(\"pointercancel\", release)\n    }\n  }, [pressed])\n\n  /* A scroll can bring an edge up against the bubble, and a scrolling box\n     the gauge sits in moves the point out from under it. */\n  useEffect(() => {\n    if (!shown) return\n    const update = () => place()\n    window.addEventListener(\"scroll\", update, true)\n    window.addEventListener(\"resize\", update)\n    return () => {\n      window.removeEventListener(\"scroll\", update, true)\n      window.removeEventListener(\"resize\", update)\n    }\n  }, [shown])\n\n  /* Every render, since an animated value moves the point frame by frame. */\n  useLayoutEffect(() => {\n    if (shown) place()\n  })\n\n  const text = (v: number) => (format ? format(v) : v.toFixed(decimals))\n  const content =\n    typeof children === \"function\"\n      ? spread\n        ? children(value, gauge.from)\n        : children(value)\n      : (children ?? (\n          <DefaultContent\n            text={\n              spread\n                ? `${text(gauge.from)}${separator}${text(value)}`\n                : text(value)\n            }\n            label={label}\n            unit={unit}\n          />\n        ))\n\n  return (\n    <>\n      <g ref={anchor} />\n      {shown &&\n        isClient &&\n        createPortal(\n          /* The wrapper holds the position so the bubble's own transform is\n             free for its entrance animation. */\n          <div\n            ref={box}\n            className=\"pointer-events-none absolute top-0 left-0 z-50\"\n          >\n            <div\n              role=\"tooltip\"\n              data-slot=\"gauge-tooltip\"\n              style={style}\n              className={cn(\n                \"w-max max-w-xs animate-in rounded-lg bg-foreground px-3 py-1.5 text-xs text-background shadow-md fade-in-0 zoom-in-95\",\n                className\n              )}\n            >\n              {content}\n            </div>\n          </div>,\n          document.body\n        )}\n    </>\n  )\n}\n\n/** The label over the value and its unit. */\nconst DefaultContent = ({\n  text,\n  label,\n  unit,\n}: {\n  text: string\n  label?: ReactNode\n  unit?: ReactNode\n}) => (\n  <div className=\"flex flex-col items-center leading-tight\">\n    {label && <span className=\"opacity-70\">{label}</span>}\n    <span className=\"text-sm font-semibold tabular-nums\">\n      {text}\n      {unit && (\n        <span className=\"ml-0.5 text-xs font-medium opacity-70\">{unit}</span>\n      )}\n    </span>\n  </div>\n)\n",
      "type": "registry:ui",
      "target": "@components/gauge/tooltip.tsx"
    },
    {
      "path": "components/gauge/control.tsx",
      "content": "\"use client\"\n\nimport {\n  useRef,\n  type KeyboardEvent,\n  type PointerEvent,\n  type ReactNode,\n} from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { angleToValue, clamp, foldValue, valueToAngle } from \"./math\"\nimport type { GaugeRange } from \"./context\"\nimport type { GaugeOrientation } from \"./scale\"\n\n/** How far out from the centre a press starts counting as the ring, as a\n    share of the half width. Inside it is the face, left for `onPress`. */\nconst FACE = 0.55\n\n/** Too near the centre for the angle under the pointer to mean anything. */\nconst HUB = 0.08\n\n/** The gauge angle under a point, measured round the element's centre. */\nconst angleAt = (el: HTMLElement, x: number, y: number) => {\n  const box = el.getBoundingClientRect()\n  const dx = x - (box.left + box.width / 2)\n  const dy = y - (box.top + box.height / 2)\n  const theta = (Math.atan2(dy, dx) * 180) / Math.PI\n  return {\n    /* Gauge degrees start at six o'clock, a quarter turn behind the maths. */\n    angle: (((theta - 90) % 360) + 360) % 360,\n    /** How far out from the centre, as a share of the half width. */\n    reach: Math.hypot(dx, dy) / (box.width / 2),\n  }\n}\n\n/**\n * The position along a bar under a point, in the SVG units of the\n * `LinearGauge` inside, read through the SVG's own screen transform so its\n * padding and scaling need no knowing.\n */\nconst positionAt = (\n  el: HTMLElement,\n  x: number,\n  y: number,\n  orientation: GaugeOrientation\n) => {\n  const ctm = el.querySelector(\"svg\")?.getScreenCTM()\n  if (!ctm) return 0\n  const p = new DOMPoint(x, y).matrixTransform(ctm.inverse())\n  /* A vertical bar runs bottom to top, against the SVG's y. */\n  return orientation === \"vertical\" ? -p.y : p.x\n}\n\n/** Degrees between two angles the short way round. */\nconst turnBetween = (a: number, b: number) =>\n  Math.abs(((((a - b) % 360) + 540) % 360) - 180)\n\n/** A literal number widened, so `value={42}` still takes any number back. */\ntype Widen<V> = V extends number ? number : [number, number]\n\nexport type GaugeControlProps<V extends number | GaugeRange = number> = {\n  /** A number for one handle, or a `[from, to]` range for two. */\n  value: V\n  onChange: (value: Widen<V>) => void\n  /** A press on the middle of the face, or Enter, for a dial that is also a\n      switch. Left out, the middle does nothing. */\n  onPress?: () => void\n  min?: number\n  max?: number\n  /** Granularity a drag lands on, and the size of an arrow-key step. */\n  step?: number\n  /** Gauge degrees, matching the gauge drawn inside. */\n  startAngle?: number\n  endAngle?: number\n  /**\n   * Make the `LinearGauge` inside a slider instead of a round gauge: a\n   * press anywhere along the bar sets the value under it, and a drag\n   * follows the pointer along the bar. Match it to the gauge's own.\n   */\n  orientation?: GaugeOrientation\n  /** Length of the bar in SVG units, matching the `LinearGauge` inside. */\n  length?: number\n  /**\n   * The domain is a closed ring, as the gauge's own `wrap`: values carry on\n   * round past either end instead of holding there, and a range may run\n   * across the seam, its ends in either order.\n   */\n  wrap?: boolean\n  /** Read to screen readers; every slider needs one. On a range it names the\n      pair, and each thumb gets its own from `thumbLabels`. */\n  label: string\n  /** Names for a range's two thumbs. Defaults to the label with \"start\" and\n      \"end\" after it. */\n  thumbLabels?: readonly [string, string]\n  /** The value as it would be spoken: \"21.5 °C\" rather than 21.5. A range\n      takes one for each end. */\n  valueText?: V extends number ? string : readonly [string, string]\n  /** The face turns like a knob: a drag anywhere on it moves the value by as\n      far as the pointer goes round, rather than jumping to where it is. A\n      range turns as a whole, keeping its width. A bar has no face, so on one\n      every drag moves the value by as far as the pointer goes along it. */\n  knob?: boolean\n  disabled?: boolean\n  className?: string\n  children: ReactNode\n}\n\n/**\n * Makes the square gauge inside it a slider: drag round the ring to set it,\n * or use the arrow keys, Page Up and Down, Home and End. With an\n * `orientation` it makes a `LinearGauge` one instead, dragged along the bar. A drag past either\n * end of the sweep holds there rather than jumping to the other. A press in\n * the middle of the face, or Enter, is left for `onPress`, which a dimmer\n * uses to switch on and off, or, with `knob`, turns the face from where it\n * is. The domain and angles default to `Gauge`'s own.\n *\n * Given a `[from, to]` range it has two thumbs: a press on the ring takes\n * whichever is nearer, and each can be tabbed to and keyed on its own. On an\n * open domain they hold short of passing each other.\n */\nexport const GaugeControl = <V extends number | GaugeRange = number>({\n  value,\n  onChange,\n  onPress,\n  min = 0,\n  max = 100,\n  step = 1,\n  startAngle = 40,\n  endAngle = 320,\n  orientation,\n  length = 400,\n  wrap = false,\n  label,\n  thumbLabels,\n  valueText,\n  knob = false,\n  disabled = false,\n  className,\n  children,\n}: GaugeControlProps<V>) => {\n  const range = typeof value !== \"number\"\n  /* Both kinds are worked on as a list of ends: one for a value, two for a\n     range. */\n  const ends: number[] = range ? [...(value as GaugeRange)] : [value as number]\n  const emit = (next: number[]) =>\n    (onChange as (value: number | [number, number]) => void)(\n      range ? [next[0], next[1]] : next[0]\n    )\n\n  /* Whether the press that is down started in the middle of the face. */\n  const pressed = useRef(false)\n  /* The end a drag on the ring is moving. */\n  const active = useRef(0)\n  /* A knob turn under way: the spot last seen, the ends when it began, and\n     how far it has moved them before rounding to a step. */\n  const turn = useRef<{ at: number; base: number[]; by: number } | null>(null)\n  const thumbs = useRef<(HTMLDivElement | null)[]>([])\n\n  const lo = Math.min(min, max)\n  const hi = Math.max(min, max)\n  const snap = (v: number) => Math.round(v / step) * step\n  const settle = (v: number) => {\n    const n = Number(v.toFixed(2))\n    return wrap ? foldValue(n, lo, hi) : clamp(n, lo, hi)\n  }\n\n  /* Moves one end, holding it short of the other on an open domain. */\n  const setEnd = (i: number, next: number) => {\n    let v = settle(next)\n    if (range && !wrap) {\n      v = i === 0 ? Math.min(v, ends[1]) : Math.max(v, ends[0])\n    }\n    emit(ends.map((e, j) => (j === i ? v : e)))\n  }\n\n  /* The scale is worked on in its own units, degrees round a ring or SVG\n     units along a bar, as the gauge inside lays it out. */\n  const linear = orientation !== undefined\n  const sweep = linear ? length : endAngle - startAngle\n  const positionOf = (v: number) =>\n    linear\n      ? valueToAngle(v, min, max, -length / 2, length / 2)\n      : valueToAngle(v, min, max, startAngle, endAngle)\n\n  /** Where a point falls on the scale, and for a ring how far out from the\n      centre it is. A bar has no face, so all of it is the scale. */\n  const locate = (el: HTMLElement, x: number, y: number) => {\n    if (orientation) {\n      return { at: positionAt(el, x, y, orientation), reach: Infinity }\n    }\n    const { angle, reach } = angleAt(el, x, y)\n    return { at: angle, reach }\n  }\n\n  /* A press past either end of a bar holds there, as one in a ring's\n     opening does. */\n  const valueAt = (at: number) =>\n    snap(\n      linear\n        ? min + clamp((at + length / 2) / length, 0, 1) * (max - min)\n        : angleToValue(at, min, max, startAngle, endAngle)\n    )\n\n  /* The end nearer the pointer, by angle so a ring measures across its seam.\n     Two ends on top of each other part the way the press lies. */\n  const nearest = (at: number, under: number) => {\n    if (!range) return 0\n    const [a, b] = ends.map((e) =>\n      linear ? Math.abs(at - positionOf(e)) : turnBetween(at, positionOf(e))\n    )\n    if (a !== b) return a < b ? 0 : 1\n    return under > ends[1] ? 1 : 0\n  }\n\n  const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {\n    if (disabled) return\n    const el = e.currentTarget\n    const { at, reach } = locate(el, e.clientX, e.clientY)\n    /* A whole bar turns as a knob does, there being no face to hold it. */\n    const face = linear ? knob : reach < FACE\n    if (face && knob) {\n      el.setPointerCapture(e.pointerId)\n      turn.current = { at, base: ends, by: 0 }\n      return\n    }\n    if (face) {\n      pressed.current = true\n      return\n    }\n    el.setPointerCapture(e.pointerId)\n    const under = valueAt(at)\n    active.current = nearest(at, under)\n    if (range) {\n      /* Focus the thumb being dragged, as a native slider does, and keep\n         the press from moving focus off it again. Browsers count focus from\n         script as keyboard focus, so the thumb is marked as taken by the\n         pointer until a key is pressed on it: the ring here and a\n         GaugeTooltip both leave a marked thumb alone. */\n      e.preventDefault()\n      const thumb = thumbs.current[active.current]\n      if (thumb) {\n        thumb.dataset.pointer = \"\"\n        thumb.focus({ preventScroll: true })\n      }\n    }\n    setEnd(active.current, under)\n  }\n\n  const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {\n    if (!e.currentTarget.hasPointerCapture(e.pointerId)) return\n    const t = turn.current\n    if (t) {\n      const { at, reach } = locate(e.currentTarget, e.clientX, e.clientY)\n      if (reach < HUB) return\n      /* Round a ring, the shortest way from the last angle, so crossing six o'clock\n         is a small step and not a whole turn back. */\n      const delta = linear ? at - t.at : ((at - t.at + 540) % 360) - 180\n      let by = t.by + (delta / sweep) * (max - min)\n      /* Held so every end stays inside an open domain, which keeps a\n         range's width as it turns. */\n      if (!wrap) {\n        by = clamp(by, lo - Math.min(...t.base), hi - Math.max(...t.base))\n      }\n      turn.current = { ...t, at, by }\n      emit(t.base.map((v) => settle(v + snap(by))))\n      return\n    }\n    const { at } = locate(e.currentTarget, e.clientX, e.clientY)\n    setEnd(active.current, valueAt(at))\n  }\n\n  const onPointerUp = () => {\n    if (pressed.current) onPress?.()\n    pressed.current = false\n    turn.current = null\n  }\n\n  const keyDown = (i: number) => (e: KeyboardEvent<HTMLDivElement>) => {\n    delete e.currentTarget.dataset.pointer\n    if (disabled) return\n    const v = ends[i]\n    const big = (max - min) / 10\n    const moves: Record<string, number> = {\n      ArrowUp: v + step,\n      ArrowRight: v + step,\n      ArrowDown: v - step,\n      ArrowLeft: v - step,\n      PageUp: v + big,\n      PageDown: v - big,\n      Home: min,\n      End: max,\n    }\n    if (e.key in moves) {\n      e.preventDefault()\n      setEnd(i, moves[e.key])\n    } else if ((e.key === \"Enter\" || e.key === \" \") && onPress) {\n      e.preventDefault()\n      onPress()\n    }\n  }\n\n  /* The ARIA a thumb carries; a lone value's is the wrapper itself. */\n  const slider = (i: number) => {\n    const texts = valueText as string | readonly [string, string] | undefined\n    return {\n      role: \"slider\",\n      tabIndex: disabled ? -1 : 0,\n      \"aria-label\": range\n        ? (thumbLabels?.[i] ?? `${label} ${i === 0 ? \"start\" : \"end\"}`)\n        : label,\n      /* On an open domain each thumb stops at the other. */\n      \"aria-valuemin\": range && !wrap && i === 1 ? ends[0] : min,\n      \"aria-valuemax\": range && !wrap && i === 0 ? ends[1] : max,\n      \"aria-valuenow\": ends[i],\n      \"aria-valuetext\": typeof texts === \"string\" ? texts : texts?.[i],\n      \"aria-orientation\": orientation,\n      \"aria-disabled\": disabled,\n      onKeyDown: keyDown(i),\n    }\n  }\n\n  return (\n    <div\n      {...(range ? { role: \"group\", \"aria-label\": label } : slider(0))}\n      data-slot=\"gauge-control\"\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerUp}\n      onPointerCancel={() => {\n        pressed.current = false\n        turn.current = null\n      }}\n      className={cn(\n        \"relative touch-none outline-none select-none focus-visible:ring-2 focus-visible:ring-ring has-[:focus-visible:not([data-pointer])]:ring-2 has-[:focus-visible:not([data-pointer])]:ring-ring\",\n        linear ? \"rounded-lg\" : \"aspect-square rounded-full\",\n        disabled ? \"cursor-default\" : \"cursor-grab active:cursor-grabbing\",\n        className\n      )}\n    >\n      {children}\n      {/* A range's thumbs are drawn by the gauge; these only take focus and\n          keys, one per end, and speak for it. */}\n      {range &&\n        ends.map((_, i) => (\n          <div\n            key={i}\n            ref={(el) => {\n              thumbs.current[i] = el\n            }}\n            {...slider(i)}\n            onBlur={(e) => delete e.currentTarget.dataset.pointer}\n            className=\"sr-only\"\n          />\n        ))}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/control.tsx"
    },
    {
      "path": "components/gauge/scale.ts",
      "content": "/**\n * The line a gauge lays its domain out along. A dial bends it round an arc\n * and a bar runs it straight. Every primitive that draws along the scale\n * asks it where a position is rather than doing the geometry itself, which\n * is what lets one set of parts draw both.\n *\n * Positions are in the scale's own units: gauge degrees round a dial, SVG\n * units along a bar. An offset moves a point off the scale's reference line,\n * outward: away from a dial's centre, up from a horizontal bar and to the\n * left of a vertical one.\n */\n\nimport { arcBox, arcPath, polar } from \"./math\"\n\nexport type GaugePoint = { x: number; y: number }\nexport type GaugeBox = { x: number; y: number; width: number; height: number }\n/** Which way a bar runs. A vertical one fills upward. */\nexport type GaugeOrientation = \"horizontal\" | \"vertical\"\n\nexport type GaugeScale = {\n  /** Position of the start of the domain. */\n  start: number\n  /** Position of the end of the domain. */\n  end: number\n  /** The scale runs right round and meets itself, as a full ring does, so\n      its two ends land on the same spot. */\n  closed: boolean\n  /** The point at a position, `offset` outward from the reference line. */\n  point: (position: number, offset?: number) => GaugePoint\n  /**\n   * SVG path along the scale between two positions, `offset` outward. Empty\n   * when `to` is not past `from`. A span carried past the end of a bar, as a\n   * wrapped arc is, comes back in at its start.\n   */\n  path: (from: number, to: number, offset?: number) => string\n  /** The outward direction at a position, as a unit vector. */\n  normal: (position: number) => GaugePoint\n  /**\n   * Box round the whole scale with its reference line moved `offset`\n   * outward. A dial's takes in its centre too, where a needle pivots.\n   */\n  box: (offset?: number) => GaugeBox\n}\n\n/** A dial's scale: a circle of `radius` swept from one gauge angle to another. */\nexport const arcScale = (\n  radius: number,\n  startAngle: number,\n  endAngle: number\n): GaugeScale => ({\n  start: startAngle,\n  end: endAngle,\n  closed: endAngle - startAngle >= 360,\n  point: (angle, offset = 0) => polar(radius + offset, angle),\n  path: (from, to, offset = 0) => arcPath(radius + offset, from, to),\n  normal: (angle) => polar(1, angle),\n  box: (offset = 0) => arcBox(radius + offset, startAngle, endAngle),\n})\n\n/**\n * A bar's scale: a straight line of `length` centred on the origin, so text\n * placed at the gauge's centre sits at the middle of the bar. A vertical bar\n * is a horizontal one turned a quarter turn anticlockwise: it starts at the\n * bottom, and its outside is on the left.\n */\nexport const lineScale = (\n  orientation: GaugeOrientation,\n  length: number\n): GaugeScale => {\n  const half = length / 2\n  const vertical = orientation === \"vertical\"\n\n  const point = (position: number, offset = 0) =>\n    vertical\n      ? { x: round(-offset), y: round(-position) }\n      : { x: round(position), y: round(-offset) }\n\n  const segment = (from: number, to: number, offset: number) => {\n    const a = point(from, offset)\n    const b = point(to, offset)\n    return `M ${a.x} ${a.y} L ${b.x} ${b.y}`\n  }\n\n  return {\n    start: -half,\n    end: half,\n    closed: false,\n    point,\n    path: (from, to, offset = 0) => {\n      if (to <= from || length <= 0) return \"\"\n      if (to <= half) return segment(from, to, offset)\n      const over = Math.min(to - half, length)\n      return [\n        from < half ? segment(from, half, offset) : \"\",\n        segment(-half, -half + over, offset),\n      ]\n        .filter(Boolean)\n        .join(\" \")\n    },\n    normal: () => (vertical ? { x: -1, y: 0 } : { x: 0, y: -1 }),\n    box: (offset = 0) => {\n      const o = Math.abs(offset)\n      return vertical\n        ? { x: -o, y: -half - o, width: 2 * o, height: length + 2 * o }\n        : { x: -half - o, y: -o, width: length + 2 * o, height: 2 * o }\n    },\n  }\n}\n\n/* Rounded as `polar` is, so server and client render identical attributes. */\nconst round = (n: number) => Math.round(n * 1000) / 1000\n",
      "type": "registry:ui",
      "target": "@components/gauge/scale.ts"
    },
    {
      "path": "components/gauge/math.ts",
      "content": "/**\n * Pure geometry for the gauge primitives.\n *\n * Angles are \"gauge degrees\": 0 points straight down (six o'clock) and\n * values increase clockwise, so a sweep from 40° to 320° leaves an opening\n * at the bottom. Coordinates are SVG units centred on the gauge origin.\n */\n\nexport const clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value))\n\n/**\n * A value folded back inside [min, max) modulo the span, as on a closed ring:\n * 370° on a compass reads as 10°, and 25 o'clock as 1.\n */\nexport const foldValue = (value: number, min: number, max: number) => {\n  const lo = Math.min(min, max)\n  const span = Math.abs(max - min)\n  return span === 0 ? lo : lo + ((((value - lo) % span) + span) % span)\n}\n\nexport const degreesToRadians = (degrees: number) => (degrees * Math.PI) / 180\n\n/**\n * Point on a circle of `radius` at a gauge angle. Coordinates are rounded so\n * server and client render identical attributes and hydration stays clean.\n */\nexport const polar = (radius: number, angle: number) => {\n  const theta = degreesToRadians(angle + 90)\n  return {\n    x: round(radius * Math.cos(theta)),\n    y: round(radius * Math.sin(theta)),\n  }\n}\n\nconst round = (n: number) => Math.round(n * 1000) / 1000\n\n/** Linear map of a value in [min, max] onto the arc [startAngle, endAngle]. */\nexport const valueToAngle = (\n  value: number,\n  min: number,\n  max: number,\n  startAngle: number,\n  endAngle: number\n) => {\n  const span = max - min\n  const t = span === 0 ? 0 : (clamp(value, min, max) - min) / span\n  return startAngle + t * (endAngle - startAngle)\n}\n\n/**\n * Angle for a value that wraps the arc more than once. A single turn maps the\n * domain onto the sweep exactly as `valueToAngle` does; past that each turn\n * runs the sweep again from its start, which is how a clock's minute hand\n * gets round twelve times while the hour hand goes round once.\n */\nexport const valueToTurnedAngle = (\n  value: number,\n  min: number,\n  max: number,\n  startAngle: number,\n  endAngle: number,\n  turns: number\n) => {\n  if (turns === 1) return valueToAngle(value, min, max, startAngle, endAngle)\n  const span = max - min\n  const t = span === 0 ? 0 : (value - min) / span\n  return startAngle + ((t * turns) % 1) * (endAngle - startAngle)\n}\n\n/**\n * SVG path for an arc of `radius` from `from` to `to` gauge degrees. Sweeps\n * over 180° are split so a single `A` command never has to disambiguate a\n * large arc, which also makes a full 360° circle render correctly.\n */\nexport const arcPath = (radius: number, from: number, to: number) => {\n  const sweep = Math.min(to - from, 360)\n  if (sweep <= 0 || radius <= 0) return \"\"\n\n  const pieces = Math.max(1, Math.ceil(sweep / 180))\n  const step = sweep / pieces\n  const start = polar(radius, from)\n  const parts = [`M ${fmt(start.x)} ${fmt(start.y)}`]\n\n  for (let i = 1; i <= pieces; i++) {\n    const p = polar(radius, from + step * i)\n    parts.push(`A ${fmt(radius)} ${fmt(radius)} 0 0 1 ${fmt(p.x)} ${fmt(p.y)}`)\n  }\n\n  return parts.join(\" \")\n}\n\n/**\n * Tight bounding box of an arc sweep, inflated by `padding`. Takes the two\n * endpoints, every quarter turn the sweep crosses, and the origin, so a gauge\n * that pivots a needle keeps its hub inside the box. A sweep that closes on\n * itself crosses all four quarters and lands back on the full square.\n */\nexport const arcBox = (\n  radius: number,\n  from: number,\n  to: number,\n  padding = 0\n) => {\n  const sweep = clamp(to - from, 0, 360)\n  const end = from + sweep\n  const points = [{ x: 0, y: 0 }, polar(radius, from), polar(radius, end)]\n  for (let a = Math.ceil(from / 90) * 90; a <= end; a += 90) {\n    points.push(polar(radius, a))\n  }\n\n  const xs = points.map((p) => p.x)\n  const ys = points.map((p) => p.y)\n  const x = round(Math.min(...xs) - padding)\n  const y = round(Math.min(...ys) - padding)\n\n  return {\n    x,\n    y,\n    width: round(Math.max(...xs) + padding - x),\n    height: round(Math.max(...ys) + padding - y),\n  }\n}\n\n/** Evenly spaced values across the domain, inclusive of both ends. */\nexport const stepValues = (min: number, max: number, intervals: number) => {\n  const n = Math.max(1, Math.round(intervals))\n  return Array.from({ length: n + 1 }, (_, i) => min + ((max - min) * i) / n)\n}\n\n/**\n * The cutoffs that split the domain into `segments` equal parts: the values\n * between the segments, none at the two ends. Ten glasses of water make nine\n * cuts.\n */\nexport const cutValues = (min: number, max: number, segments: number) =>\n  stepValues(min, max, segments).slice(1, -1)\n\n/**\n * Inverse of `valueToAngle`: the domain value under a gauge angle. An angle\n * in the opening outside the sweep goes to whichever end is nearer, so a\n * pointer dragged past an end holds there rather than jumping to the other.\n */\nexport const angleToValue = (\n  angle: number,\n  min: number,\n  max: number,\n  startAngle: number,\n  endAngle: number\n) => {\n  let a = angle\n  while (a < startAngle) a += 360\n  while (a >= startAngle + 360) a -= 360\n  if (a > endAngle) {\n    a = a - endAngle < startAngle + 360 - a ? endAngle : startAngle\n  }\n  const sweep = endAngle - startAngle\n  const t = sweep === 0 ? 0 : (a - startAngle) / sweep\n  return min + t * (max - min)\n}\n\nconst fmt = (n: number) => Number(n.toFixed(3)).toString()\n",
      "type": "registry:ui",
      "target": "@components/gauge/math.ts"
    },
    {
      "path": "components/gauge/utils.ts",
      "content": "/**\n * Shared vocabulary and pure helpers for the gauge primitives: stroke and\n * font options, zones, and the formatters that turn a value into a label.\n * Everything here is framework-free so it can be used in generated code.\n */\n\nexport type StrokeCap = \"round\" | \"butt\"\nexport type FontFamily = \"sans\" | \"rounded\" | \"mono\"\nexport type FontWeight = \"regular\" | \"medium\" | \"semibold\" | \"bold\"\nexport type NeedleStyle = \"line\" | \"pointer\" | \"compass\" | \"arrow\"\n/**\n * Which end of a range a part follows: `to`, the far end, which is the value\n * itself on a single-value gauge, or `from`, where the range starts.\n */\nexport type GaugeEnd = \"from\" | \"to\"\n/** A colour for every mark alike, or one per mark worked out from its value. */\nexport type ValueColor = string | ((value: number) => string)\n/** Which edge of a text sits at its position: left, centre or right. */\nexport type TextAnchor = \"start\" | \"middle\" | \"end\"\n\nexport type Zone = {\n  /** Upper cutoff of the zone in domain units. Ignored for the last zone. */\n  to: number\n  color: string\n}\n\nexport const fontClass: Record<FontFamily, string> = {\n  sans: \"font-sans\",\n  rounded: \"font-rounded\",\n  mono: \"font-mono\",\n}\n\nexport const weightClass: Record<FontWeight, string> = {\n  regular: \"font-normal\",\n  medium: \"font-medium\",\n  semibold: \"font-semibold\",\n  bold: \"font-bold\",\n}\n\n/** Colour of the zone a value falls in, or `fallback` when there are none. */\nexport const zoneColor = (zones: Zone[], value: number, fallback: string) => {\n  if (zones.length === 0) return fallback\n  for (let i = 0; i < zones.length - 1; i++) {\n    if (value < zones[i].to) return zones[i].color\n  }\n  return zones[zones.length - 1].color\n}\n\n/** Cutoff values that separate neighbouring zones. */\nexport const zoneCutoffs = (zones: Zone[]) =>\n  zones.slice(0, -1).map((zone) => zone.to)\n\nconst compassPoints = [\n  \"N\",\n  \"NNE\",\n  \"NE\",\n  \"ENE\",\n  \"E\",\n  \"ESE\",\n  \"SE\",\n  \"SSE\",\n  \"S\",\n  \"SSW\",\n  \"SW\",\n  \"WSW\",\n  \"W\",\n  \"WNW\",\n  \"NW\",\n  \"NNW\",\n]\n\n/**\n * Formats a heading in degrees as a compass point when it lands on one of the\n * sixteen, and as degrees otherwise, so a 0–360 domain reads like a bezel:\n * N, 30°, 60°, E, and so on.\n */\nexport const compassLabel = (degrees: number) => {\n  const turn = ((degrees % 360) + 360) % 360\n  const index = Math.round(turn / 22.5) % 16\n  const exact = Math.abs(turn - index * 22.5) < 0.5\n  return exact ? compassPoints[index] : `${Math.round(turn)}°`\n}\n\n/** Formats a value in hours as a clock numeral, so 0 and 12 both read \"12\". */\nexport const clockLabel = (hours: number) =>\n  String(((Math.round(hours) % 12) + 12) % 12 || 12)\n\nconst pad = (n: number) => String(n).padStart(2, \"0\")\n\n/**\n * Hours past midnight as a 24-hour clock time: 6.5 reads \"06:30\". Negative\n * hours reach back into the day before, and past 24 into the day after.\n */\nexport const clockTime = (hours: number) => {\n  const minutes = Math.round((((hours % 24) + 24) % 24) * 60)\n  return `${pad(Math.floor(minutes / 60) % 24)}:${pad(minutes % 60)}`\n}\n\n/** A duration in hours as \"7h 32m\". */\nexport const durationLabel = (hours: number) => {\n  const minutes = Math.round(hours * 60)\n  return `${Math.floor(minutes / 60)}h ${pad(minutes % 60)}m`\n}\n\n/**\n * `color` thinned towards transparent: the one ink of a face taken down to\n * its quieter tones, holding apart in either theme.\n */\nexport const fadeColor = (color: string, percent: number) =>\n  `color-mix(in oklch, ${color} ${percent}%, transparent)`\n",
      "type": "registry:ui",
      "target": "@components/gauge/utils.ts"
    },
    {
      "path": "components/gauge/transition.ts",
      "content": "/**\n * Dependency-free value animation for the gauge: a damped spring and a\n * cubic-bezier tween, both stepped per frame. Pure functions only; the hook\n * that drives them lives in `use-animated-value.ts`.\n */\n\n/** A named curve, CSS-style cubic-bezier control points, or your own function. */\nexport type GaugeEasing =\n  | \"linear\"\n  | \"easeIn\"\n  | \"easeOut\"\n  | \"easeInOut\"\n  | [number, number, number, number]\n  | ((t: number) => number)\n\nexport type GaugeSpringTransition = {\n  type: \"spring\"\n  /** Physics parameters. When any is given they take precedence over the\n      perceptual `visualDuration` and `bounce` pair. */\n  stiffness?: number\n  damping?: number\n  mass?: number\n  /** Seconds the spring visibly takes to reach the target. Default 0.5. */\n  visualDuration?: number\n  /** 0 settles without overshoot, 1 rings freely. Default 0.1. */\n  bounce?: number\n}\n\nexport type GaugeTweenTransition = {\n  type: \"tween\"\n  /** Seconds. Default 0.5. */\n  duration?: number\n  /** Default \"easeInOut\". */\n  ease?: GaugeEasing\n}\n\nexport type GaugeTransition = GaugeSpringTransition | GaugeTweenTransition\n\nexport const DEFAULT_TRANSITION: GaugeSpringTransition = {\n  type: \"spring\",\n  visualDuration: 0.5,\n  bounce: 0.1,\n}\n\n/** Turns the `transition` prop into a concrete config, or null when off. */\nexport const resolveTransition = (\n  transition: GaugeTransition | boolean | undefined\n): GaugeTransition | null => {\n  if (!transition) return null\n  return transition === true ? DEFAULT_TRANSITION : transition\n}\n\n/* ---------- spring ---------- */\n\nexport type SpringPhysics = { stiffness: number; damping: number; mass: number }\n\n/**\n * Physics for a spring config. The perceptual pair maps onto stiffness and\n * damping the same way the motion library does, so a spring tuned against\n * that library feels the same here.\n */\nexport const springPhysics = (t: GaugeSpringTransition): SpringPhysics => {\n  const hasPhysics =\n    t.stiffness !== undefined || t.damping !== undefined || t.mass !== undefined\n  if (hasPhysics) {\n    return {\n      stiffness: t.stiffness ?? 100,\n      damping: t.damping ?? 10,\n      mass: t.mass ?? 1,\n    }\n  }\n  const visualDuration = t.visualDuration ?? 0.5\n  const bounce = t.bounce ?? 0.1\n  const root = (2 * Math.PI) / (visualDuration * 1.2)\n  const stiffness = root * root\n  const ratio = Math.min(1, Math.max(0.05, 1 - bounce))\n  return { stiffness, damping: 2 * ratio * Math.sqrt(stiffness), mass: 1 }\n}\n\nexport type SpringState = { value: number; velocity: number }\n\nconst SUBSTEP = 1 / 240\n\n/**\n * Advances a spring by `dt` seconds towards `target` with semi-implicit\n * Euler in small substeps, which stays stable for stiff springs. Returns the\n * new state and whether it has come to rest within `precision`.\n */\nexport const stepSpring = (\n  state: SpringState,\n  target: number,\n  physics: SpringPhysics,\n  dt: number,\n  precision: number\n): { state: SpringState; done: boolean } => {\n  let { value, velocity } = state\n  let remaining = Math.min(dt, 0.064)\n  while (remaining > 0) {\n    const h = Math.min(SUBSTEP, remaining)\n    const force =\n      -physics.stiffness * (value - target) - physics.damping * velocity\n    velocity += (force / physics.mass) * h\n    value += velocity * h\n    remaining -= h\n  }\n  const done =\n    Math.abs(value - target) < precision && Math.abs(velocity) < precision * 10\n  return done\n    ? { state: { value: target, velocity: 0 }, done }\n    : { state: { value, velocity }, done }\n}\n\n/* ---------- tween ---------- */\n\nconst namedCurves: Record<\n  Exclude<\n    GaugeEasing,\n    [number, number, number, number] | ((t: number) => number)\n  >,\n  [number, number, number, number]\n> = {\n  linear: [0, 0, 1, 1],\n  easeIn: [0.42, 0, 1, 1],\n  easeOut: [0, 0, 0.58, 1],\n  easeInOut: [0.42, 0, 0.58, 1],\n}\n\n/** A CSS-compatible cubic-bezier easing function. */\nexport const cubicBezier = (x1: number, y1: number, x2: number, y2: number) => {\n  const bezier = (t: number, p1: number, p2: number) => {\n    const u = 1 - t\n    return 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t\n  }\n  const slope = (t: number, p1: number, p2: number) => {\n    const u = 1 - t\n    return 3 * u * u * p1 + 6 * u * t * (p2 - p1) + 3 * t * t * (1 - p2)\n  }\n  return (x: number) => {\n    if (x <= 0) return 0\n    if (x >= 1) return 1\n    // Newton's method, falling back to bisection where the curve is flat.\n    let t = x\n    for (let i = 0; i < 8; i++) {\n      const dx = bezier(t, x1, x2) - x\n      const d = slope(t, x1, x2)\n      if (Math.abs(dx) < 1e-6) return bezier(t, y1, y2)\n      if (Math.abs(d) < 1e-6) break\n      t -= dx / d\n    }\n    let lo = 0\n    let hi = 1\n    t = x\n    while (hi - lo > 1e-6) {\n      if (bezier(t, x1, x2) < x) lo = t\n      else hi = t\n      t = (lo + hi) / 2\n    }\n    return bezier(t, y1, y2)\n  }\n}\n\n/** Resolves any `GaugeEasing` into a function of progress. */\nexport const easingFunction = (ease: GaugeEasing = \"easeInOut\") => {\n  if (typeof ease === \"function\") return ease\n  const [x1, y1, x2, y2] = Array.isArray(ease) ? ease : namedCurves[ease]\n  return cubicBezier(x1, y1, x2, y2)\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/transition.ts"
    },
    {
      "path": "components/gauge/use-animated-value.ts",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\n\nimport {\n  easingFunction,\n  resolveTransition,\n  springPhysics,\n  stepSpring,\n  type GaugeTransition,\n  type SpringState,\n} from \"./transition\"\n\n/** Folds a change into [-span/2, span/2), the short way round a circle. */\nconst shortestDelta = (delta: number, span: number) => {\n  const s = Math.max(span, 1e-9)\n  return ((((delta + s / 2) % s) + s) % s) - s / 2\n}\n\n/**\n * Follows `target` with the given transition and returns the value to draw\n * this frame. With no transition it returns `target` as is and renders\n * nothing extra. Honours `prefers-reduced-motion` by snapping.\n *\n * `span` is the size of the domain and sets the rest threshold. `initial` is\n * where the value starts on mount, for a sweep-in from the minimum. `wrap`\n * treats the domain as circular: every change is followed the shortest way\n * round, so a heading crossing north never swings the long way back. The\n * returned value may then sit outside the domain mid-flight; map it back\n * with a modulo.\n */\nexport const useAnimatedValue = (\n  target: number,\n  transition: GaugeTransition | boolean | undefined,\n  span: number,\n  initial?: number,\n  wrap?: boolean\n) => {\n  const enabled = Boolean(transition)\n  const [display, setDisplay] = useState(initial ?? target)\n  const spring = useRef<SpringState>({ value: initial ?? target, velocity: 0 })\n\n  /* With `wrap` the target is folded into a continuous series so the spring\n     crosses the seam instead of doubling back. Each change is folded in as\n     it renders — the sanctioned way to adjust state to a new prop — and the\n     series snaps back to the canonical target at rest, so turning forever\n     never drifts far. */\n  const start = initial ?? target\n  const [series, setSeries] = useState(() => ({\n    raw: target,\n    goal: wrap ? start + shortestDelta(target - start, span) : target,\n  }))\n  if (wrap && (target !== series.raw || (!enabled && target !== series.goal))) {\n    setSeries(\n      enabled\n        ? {\n            raw: target,\n            goal: series.goal + shortestDelta(target - series.raw, span),\n          }\n        : /* Without a transition the series just shadows the target, so\n             re-enabling one starts from what is on screen. */\n          { raw: target, goal: target }\n    )\n  }\n  const goal = wrap ? series.goal : target\n\n  // The config is read through a ref so an inline object or easing function\n  // does not restart the animation every render; only its content matters.\n  const transitionRef = useRef(transition)\n  useEffect(() => {\n    transitionRef.current = transition\n  })\n  const configKey = JSON.stringify(transition)\n\n  // While disabled, keep the spring state in step so re-enabling animates\n  // from what is on screen rather than from a stale value.\n  useEffect(() => {\n    if (!enabled) spring.current = { value: target, velocity: 0 }\n  }, [enabled, target])\n\n  useEffect(() => {\n    const config = resolveTransition(transitionRef.current)\n    if (!config) return\n    const precision = Math.max(span, 1e-6) * 1e-4\n    const snap = prefersReducedMotion()\n    /* Where the value comes to rest: the canonical target, so a wrapped\n       series that has gone round a few times folds back into the domain. */\n    const restAt = target\n\n    let frame = 0\n    let last = performance.now()\n    const from = spring.current.value\n    const start = last\n\n    const tick = (now: number) => {\n      const dt = (now - last) / 1000\n      last = now\n      let done: boolean\n\n      if (snap) {\n        done = true\n      } else if (config.type === \"spring\") {\n        const physics = springPhysics(config)\n        const next = stepSpring(spring.current, goal, physics, dt, precision)\n        spring.current = next.state\n        done = next.done\n      } else {\n        const duration = Math.max(config.duration ?? 0.5, 1e-3)\n        const progress = Math.min((now - start) / 1000 / duration, 1)\n        const eased = easingFunction(config.ease)(progress)\n        const value = from + (goal - from) * eased\n        spring.current = { value, velocity: 0 }\n        done = progress >= 1\n      }\n\n      if (done) {\n        spring.current = { value: restAt, velocity: 0 }\n        if (wrap) {\n          setSeries((s) =>\n            s.raw === restAt && s.goal === restAt\n              ? s\n              : { raw: restAt, goal: restAt }\n          )\n        }\n      }\n      setDisplay(spring.current.value)\n      if (!done) frame = requestAnimationFrame(tick)\n    }\n\n    frame = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(frame)\n  }, [goal, target, configKey, span, wrap])\n\n  return enabled ? display : target\n}\n\nconst prefersReducedMotion = () =>\n  typeof window !== \"undefined\" &&\n  typeof window.matchMedia === \"function\" &&\n  window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n",
      "type": "registry:ui",
      "target": "@components/gauge/use-animated-value.ts"
    }
  ],
  "docs": "Import everything from \"@/components/gauge\". Compose <Gauge value min max startAngle endAngle> with GaugeTrack, GaugeArc, GaugeZones, GaugeTicks, GaugeMarks, GaugeTickLabels, GaugeNeedle, GaugeHub, GaugeValue and GaugeText. Nest a whole gauge inside another with <GaugeInset x y scale value min max>, for a fuel dial under a speedometer or a second ring inside the first. For a straight bar use <LinearGauge value orientation length padding> in place of <Gauge>: every part but GaugeNeedle works inside it, <LinearGaugeInset> nests one in another gauge, and <GaugeControl orientation> makes it a slider. Add transition to <Gauge> to animate value changes: true for the default spring, or { type: \"spring\", visualDuration, bounce } / { type: \"tween\", duration, ease }. Design one visually at the studio and copy the generated code.",
  "categories": [
    "data-display",
    "charts"
  ],
  "type": "registry:ui"
}