{
  "$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, labels, needle, hub, dot, tooltip and value text, with optional spring or tween animation and nested gauges. No runtime dependencies.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/gauge/index.ts",
      "content": "export { Gauge, type GaugeFit, type GaugeProps } from \"./gauge\"\nexport { GaugeInset, type GaugeInsetProps } from \"./inset\"\nexport {\n  useGauge,\n  type GaugeContextValue,\n  type GaugeDomainProps,\n} from \"./context\"\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  GaugeMarks,\n  GaugeTickLabels,\n  type GaugeTicksProps,\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  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 NeedleStyle,\n  type StrokeCap,\n  type TextAnchor,\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/inset.tsx",
      "content": "\"use client\"\n\nimport type { SVGProps } from \"react\"\n\nimport { GaugeContext, useGaugeContextValue } from \"./context\"\nimport type { GaugeDomainProps } 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",
      "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, valueToAngle } from \"./math\"\nimport type { GaugeTransition } from \"./transition\"\nimport { useAnimatedValue } from \"./use-animated-value\"\n\nexport type GaugeContextValue = {\n  /** Value being drawn this frame, clamped to the domain. Animated when the\n      gauge has a transition. */\n  value: number\n  /** Value the gauge is heading for; equals `value` once settled. */\n  target: number\n  min: number\n  max: number\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. */\n  angleOf: (value: number) => number\n}\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/** The domain, geometry and animation every gauge root takes. */\nexport type GaugeDomainProps = {\n  value: number\n  min?: number\n  max?: number\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   * 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  initialValue?: number\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   * 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, returning the context a gauge\n * root publishes to its children. Shared by `Gauge` and `GaugeInset` so a\n * gauge nested inside another animates and maps values exactly like a\n * top-level one, and so the defaults live in one place.\n */\nexport const useGaugeContextValue = ({\n  value,\n  min = 0,\n  max = 100,\n  startAngle = 40,\n  endAngle = 320,\n  radius = 200,\n  transition,\n  initialValue,\n  wrap = false,\n  rotate = 0,\n}: GaugeDomainProps): GaugeContextValue => {\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) =>\n    span === 0 ? lo : lo + ((((v - lo) % span) + span) % span)\n  const settle = wrap ? fold : (v: number) => clamp(v, lo, hi)\n\n  const target = settle(value)\n  const shown = useAnimatedValue(\n    target,\n    transition,\n    span,\n    initialValue === undefined ? undefined : settle(initialValue),\n    wrap\n  )\n  /* The rotation is a bearing, so it always takes the short way round. */\n  const turned = useAnimatedValue(rotate, transition, 360, undefined, true)\n  const a0 = startAngle + turned\n  const a1 = endAngle + turned\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    min,\n    max,\n    startAngle: a0,\n    endAngle: a1,\n    radius,\n    angleOf: (v) => valueToAngle(v, min, max, a0, a1),\n  }\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/context.tsx"
    },
    {
      "path": "components/gauge/arcs.tsx",
      "content": "\"use client\"\n\nimport { arcPath, polar } 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  /** Radial offset from the reference radius. Positive moves outward. */\n  offset?: number\n}\n\n/** The full background arc from `startAngle` to `endAngle`. */\nexport const GaugeTrack = ({\n  width = 24,\n  color = \"currentColor\",\n  opacity = 0.15,\n  cap = \"round\",\n  offset = 0,\n}: ArcStyleProps) => {\n  const { radius, startAngle, endAngle } = useGauge()\n  return (\n    <path\n      d={arcPath(radius + offset, startAngle, endAngle)}\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`. */\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.\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 { radius, min, max, value, startAngle, endAngle, angleOf } = useGauge()\n  const tail = from ?? min\n  const head = to ?? value\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) {\n    const span = max - min\n    if (span <= 0) return null\n    const perUnit = (endAngle - startAngle) / span\n    a0 = startAngle + ((((lo - min) % span) + span) % span) * perUnit\n    a1 = a0 + Math.min((hi - lo) * perUnit, 360)\n  } else {\n    ;[a0, a1] = reverse\n      ? [angleOf(max - (hi - lo)), angleOf(max)]\n      : [angleOf(lo), angleOf(hi)]\n  }\n  if (a1 <= a0) return null\n\n  return (\n    <path\n      d={arcPath(radius + offset, a0, a1)}\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`. */\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 { radius, min, value, angleOf } = useGauge()\n  if (parts.length === 0) return null\n\n  const total = parts.reduce((sum, p) => sum + (p.weight ?? 0), 0)\n  const start = from ?? min\n  const fill = value - start\n  const r = radius + offset\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 = angleOf(i === 0 ? start : ends[i - 1])\n        const a1 = angleOf(Math.min(ends[i], value))\n        if (a1 <= a0) return null\n        return (\n          <path\n            key={i}\n            d={arcPath(r, a0, a1)}\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 degrees. */\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 { radius, min, max, angleOf } = useGauge()\n  if (zones.length === 0) return null\n\n  const r = radius + offset\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 = (angle: number, color: string, key: string) => {\n    const { x, y } = polar(r, angle)\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 = angleOf(from) + (isFirst ? 0 : gap / 2)\n        const a1 = angleOf(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={arcPath(r, a0, a1)}\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, polar, stepValues } from \"./math\"\nimport type { GaugeContextValue } from \"./context\"\nimport {\n  fontClass,\n  weightClass,\n  type FontFamily,\n  type FontWeight,\n  type StrokeCap,\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 = (\n  { min, max, startAngle, endAngle }: GaugeContextValue,\n  count: number\n) => {\n  const values = stepValues(min, max, count)\n  if (endAngle - startAngle >= 360) values.pop()\n  return values\n}\n\ntype RadialLineProps = {\n  /** Length of each line along the radius. */\n  length?: number\n  width?: number\n  color?: string\n  opacity?: number\n  cap?: StrokeCap\n  /** Radial offset of the line's midpoint from the reference radius. */\n  offset?: number\n}\n\nconst RadialLines = ({\n  angles,\n  length = 12,\n  width = 2,\n  color = \"currentColor\",\n  opacity = 1,\n  cap = \"round\",\n  offset = 0,\n}: RadialLineProps & { angles: number[] }) => {\n  const { radius } = useGauge()\n  const inner = radius + offset - length / 2\n  const outer = radius + offset + length / 2\n\n  return (\n    <g\n      stroke={color}\n      strokeOpacity={opacity}\n      strokeWidth={width}\n      strokeLinecap={cap}\n    >\n      {angles.map((angle, i) => {\n        const a = polar(inner, angle)\n        const b = polar(outer, angle)\n        return <line key={i} x1={a.x} y1={a.y} x2={b.x} y2={b.y} />\n      })}\n    </g>\n  )\n}\n\nexport type GaugeTicksProps = RadialLineProps & {\n  /** Number of intervals across the domain; renders `count + 1` ticks. */\n  count?: number\n}\n\n/** Evenly spaced tick marks across the whole arc. */\nexport const GaugeTicks = ({ count = 10, ...props }: GaugeTicksProps) => {\n  const gauge = useGauge()\n  const angles = tickValues(gauge, count).map(gauge.angleOf)\n  return <RadialLines angles={angles} {...props} />\n}\n\nexport type GaugeMarksProps = RadialLineProps & {\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/** Radial marks at arbitrary domain values, or at equal cuts of the domain. */\nexport const GaugeMarks = ({ values, count, ...props }: GaugeMarksProps) => {\n  const { min, max, startAngle, endAngle, angleOf } = useGauge()\n  const marks =\n    values ??\n    (count && count > 1\n      ? [\n          ...(endAngle - startAngle >= 360 ? [min] : []),\n          ...cutValues(min, max, count),\n        ]\n      : [])\n  return <RadialLines angles={marks.map(angleOf)} {...props} />\n}\n\nexport type GaugeTickLabelsProps = {\n  count?: number\n  /** Radial offset of the label centre from the reference radius. */\n  offset?: number\n  fontSize?: number\n  color?: string\n  font?: FontFamily\n  weight?: FontWeight\n  decimals?: number\n  format?: (value: number) => string\n}\n\n/** Numeric labels at evenly spaced values across the arc. */\nexport const GaugeTickLabels = ({\n  count = 10,\n  offset = -48,\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 { radius, angleOf } = gauge\n  const values = tickValues(gauge, count)\n\n  return (\n    <g\n      fill={color}\n      fontSize={fontSize}\n      textAnchor=\"middle\"\n      dominantBaseline=\"central\"\n      className={cn(fontClass[font], weightClass[weight], \"tabular-nums\")}\n    >\n      {values.map((value, i) => {\n        const p = polar(radius + offset, angleOf(value))\n        return (\n          <text key={i} x={p.x} y={p.y}>\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 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  format?: (value: number) => string\n}\n\n/** The current value as text. */\nexport const GaugeValue = ({\n  decimals = 0,\n  format,\n  className,\n  fontSize = 96,\n  weight = \"semibold\",\n  ...props\n}: GaugeValueProps) => {\n  const { value } = useGauge()\n  return (\n    <GaugeText\n      fontSize={fontSize}\n      weight={weight}\n      className={cn(\"tabular-nums\", className)}\n      {...props}\n    >\n      {format ? format(value) : value.toFixed(decimals)}\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 { NeedleStyle } from \"./utils\"\n\nimport { useGauge } 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}\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}: Omit<GaugeNeedleProps, \"gap\">) => {\n  const { radius, value, min, max, startAngle, endAngle } = useGauge()\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 { polar, valueToTurnedAngle } from \"./math\"\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  /** Radial offset of the dot's centre from the reference radius. */\n  offset?: number\n  /**\n   * Sweeps of the arc the dot makes across the domain, as `GaugeNeedle.turns`:\n   * two sends it round twice while the value crosses 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}\n\n/**\n * A disc riding the arc at the current value: the head of a progress ring, a\n * body on its orbit, the handle on a dial. `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}: GaugeDotProps) => {\n  const gauge = useGauge()\n  const angle = valueToTurnedAngle(\n    gauge.value,\n    gauge.min,\n    gauge.max,\n    gauge.startAngle,\n    gauge.endAngle,\n    turns\n  )\n  const { x, y } = polar(gauge.radius + offset, angle)\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 { arcBox, polar } 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 arc at the value, and the rest are fixed points\n * of the dial: its centre, or the middle of an edge or a corner of the box\n * its sweep 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 along the radius at the value, so the bubble swings round 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; a fixed one marks a target or a cutoff.\n   */\n  at?: number\n  /**\n   * How far out from the reference radius the point sits, in SVG units: the\n   * spot on the arc for `value`, and the box round the sweep for the edges\n   * 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  format?: (value: number) => 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.\n   */\n  children?: ReactNode | ((value: 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. */\nconst FOCUSABLE =\n  '[tabindex]:not([tabindex=\"-1\"]), a[href], button, input, select, textarea'\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  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  const angle = gauge.angleOf(value)\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 = polar(gauge.radius + offset, angle)\n    else if (position !== \"center\") {\n      const b = arcBox(gauge.radius + offset, gauge.startAngle, gauge.endAngle)\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 along the radius, stretched onto the square so the bubble's\n       nearest edge or corner meets the point at any angle. */\n    const r = polar(1, angle)\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    const check = () =>\n      setFocused(\n        document.activeElement === owner && owner.matches(\":focus-visible\")\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(document.activeElement === owner)\n    const blur = () => setFocused(false)\n    check()\n    owner.addEventListener(\"focus\", check)\n    owner.addEventListener(\"keydown\", key)\n    owner.addEventListener(\"blur\", blur)\n    return () => {\n      owner.removeEventListener(\"focus\", check)\n      owner.removeEventListener(\"keydown\", key)\n      owner.removeEventListener(\"blur\", 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 content =\n    typeof children === \"function\"\n      ? children(value)\n      : (children ?? (\n          <DefaultContent\n            text={format ? format(value) : value.toFixed(decimals)}\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 } from \"./math\"\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\nexport type GaugeControlProps = {\n  value: number\n  onChange: (value: number) => 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  /** Read to screen readers; every slider needs one. */\n  label: string\n  /** The value as it would be spoken: \"21.5 °C\" rather than 21.5. */\n  valueText?: 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. */\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. 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 */\nexport const GaugeControl = ({\n  value,\n  onChange,\n  onPress,\n  min = 0,\n  max = 100,\n  step = 1,\n  startAngle = 40,\n  endAngle = 320,\n  label,\n  valueText,\n  knob = false,\n  disabled = false,\n  className,\n  children,\n}: GaugeControlProps) => {\n  /* Whether the press that is down started in the middle of the face. */\n  const pressed = useRef(false)\n  /* A knob turn under way: the angle last seen, and the value it has got to\n     before rounding to a step. */\n  const turn = useRef<{ angle: number; value: number } | null>(null)\n\n  const set = (next: number) =>\n    onChange(clamp(Number(next.toFixed(2)), min, max))\n\n  const valueUnder = (el: HTMLElement, x: number, y: number) => {\n    const raw = angleToValue(\n      angleAt(el, x, y).angle,\n      min,\n      max,\n      startAngle,\n      endAngle\n    )\n    return Math.round(raw / step) * step\n  }\n\n  const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {\n    if (disabled) return\n    const { angle, reach } = angleAt(e.currentTarget, e.clientX, e.clientY)\n    if (reach < FACE && knob) {\n      e.currentTarget.setPointerCapture(e.pointerId)\n      turn.current = { angle, value }\n      return\n    }\n    if (reach < FACE) {\n      pressed.current = true\n      return\n    }\n    e.currentTarget.setPointerCapture(e.pointerId)\n    set(valueUnder(e.currentTarget, e.clientX, e.clientY))\n  }\n\n  const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {\n    if (!e.currentTarget.hasPointerCapture(e.pointerId)) return\n    if (turn.current) {\n      const { angle, reach } = angleAt(e.currentTarget, e.clientX, e.clientY)\n      if (reach < HUB) return\n      /* The shortest way round from the last angle, so crossing six o'clock\n         is a small step and not a whole turn back. */\n      const by = ((angle - turn.current.angle + 540) % 360) - 180\n      const value = clamp(\n        turn.current.value + (by / (endAngle - startAngle)) * (max - min),\n        min,\n        max\n      )\n      turn.current = { angle, value }\n      set(Math.round(value / step) * step)\n      return\n    }\n    set(valueUnder(e.currentTarget, e.clientX, e.clientY))\n  }\n\n  const onPointerUp = () => {\n    if (pressed.current) onPress?.()\n    pressed.current = false\n    turn.current = null\n  }\n\n  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    if (disabled) return\n    const big = (max - min) / 10\n    const moves: Record<string, number> = {\n      ArrowUp: value + step,\n      ArrowRight: value + step,\n      ArrowDown: value - step,\n      ArrowLeft: value - step,\n      PageUp: value + big,\n      PageDown: value - big,\n      Home: min,\n      End: max,\n    }\n    if (e.key in moves) {\n      e.preventDefault()\n      set(moves[e.key])\n    } else if ((e.key === \"Enter\" || e.key === \" \") && onPress) {\n      e.preventDefault()\n      onPress()\n    }\n  }\n\n  return (\n    <div\n      role=\"slider\"\n      tabIndex={disabled ? -1 : 0}\n      aria-label={label}\n      aria-valuemin={min}\n      aria-valuemax={max}\n      aria-valuenow={value}\n      aria-valuetext={valueText}\n      aria-disabled={disabled}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerUp}\n      onPointerCancel={() => {\n        pressed.current = false\n        turn.current = null\n      }}\n      onKeyDown={onKeyDown}\n      className={cn(\n        \"aspect-square touch-none rounded-full outline-none select-none focus-visible:ring-2 focus-visible:ring-ring\",\n        disabled ? \"cursor-default\" : \"cursor-grab active:cursor-grabbing\",\n        className\n      )}\n    >\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/gauge/control.tsx"
    },
    {
      "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\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/** 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. 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"
}