{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gauge-chart",
  "type": "registry:component",
  "title": "Gauge",
  "description": "Notch-based radial or linear gauge with optional center label, patterns, and optional gradients",
  "dependencies": [
    "@visx/responsive@4.0.1-alpha.0",
    "@visx/pattern@4.0.1-alpha.0",
    "@number-flow/react",
    "motion",
    "d3-shape"
  ],
  "registryDependencies": [
    "@bklit/utils"
  ],
  "files": [
    {
      "path": "src/charts/chart-stat-flow.tsx",
      "content": "\"use client\";\n\nimport NumberFlow from \"@number-flow/react\";\nimport { type ReactNode, useEffect, useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/** Subset of `Intl.NumberFormatOptions` supported by NumberFlow */\nexport interface ChartStatFlowFormat {\n  notation?: \"standard\" | \"compact\";\n  compactDisplay?: \"short\" | \"long\";\n  minimumFractionDigits?: number;\n  maximumFractionDigits?: number;\n  minimumIntegerDigits?: number;\n  minimumSignificantDigits?: number;\n  maximumSignificantDigits?: number;\n  style?: \"decimal\" | \"percent\" | \"currency\";\n  currency?: string;\n  currencyDisplay?: \"symbol\" | \"narrowSymbol\" | \"code\" | \"name\";\n  unit?: string;\n  unitDisplay?: \"short\" | \"long\" | \"narrow\";\n}\n\nexport const defaultChartStatFlowFormat: ChartStatFlowFormat = {\n  notation: \"standard\",\n  maximumFractionDigits: 0,\n};\n\nfunction formatStatValue(\n  value: number,\n  formatOptions: ChartStatFlowFormat,\n  prefix?: string,\n  suffix?: string\n): string {\n  const formatted = new Intl.NumberFormat(undefined, formatOptions).format(\n    value\n  );\n  return `${prefix ?? \"\"}${formatted}${suffix ?? \"\"}`;\n}\n\nfunction useNumberFlowElementReady(): boolean {\n  const [ready, setReady] = useState(\n    () =>\n      typeof customElements !== \"undefined\" &&\n      Boolean(customElements.get(\"number-flow-react\"))\n  );\n\n  useEffect(() => {\n    if (ready) {\n      return;\n    }\n    let cancelled = false;\n    customElements.whenDefined(\"number-flow-react\").then(() => {\n      if (!cancelled) {\n        setReady(true);\n      }\n    });\n    return () => {\n      cancelled = true;\n    };\n  }, [ready]);\n\n  return ready;\n}\n\nexport interface ChartStatFlowProps {\n  value: number;\n  label: string;\n  formatOptions?: ChartStatFlowFormat;\n  prefix?: string;\n  suffix?: string;\n  valueClassName?: string;\n  labelClassName?: string;\n  icon?: ReactNode;\n}\n\n/**\n * Shared value + label stack using NumberFlow (same layout as pie / ring centers).\n * Parent should provide flex alignment and sizing when needed.\n */\nexport function ChartStatFlow({\n  value,\n  label,\n  formatOptions = defaultChartStatFlowFormat,\n  prefix,\n  suffix,\n  valueClassName = \"text-2xl font-bold\",\n  labelClassName = \"text-xs\",\n  icon,\n}: ChartStatFlowProps) {\n  const numberFlowReady = useNumberFlowElementReady();\n  const staticValue = useMemo(\n    () => formatStatValue(value, formatOptions, prefix, suffix),\n    [value, formatOptions, prefix, suffix]\n  );\n\n  return (\n    <>\n      {icon ? (\n        <div className=\"mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-muted/50\">\n          {icon}\n        </div>\n      ) : null}\n      <span className={cn(\"text-foreground tabular-nums\", valueClassName)}>\n        {numberFlowReady ? (\n          <NumberFlow\n            format={formatOptions}\n            isolate\n            prefix={prefix}\n            suffix={suffix}\n            value={value}\n            willChange\n          />\n        ) : (\n          staticValue\n        )}\n      </span>\n      <span className={cn(\"mt-0.5 text-chart-label\", labelClassName)}>\n        {label}\n      </span>\n    </>\n  );\n}\n\nChartStatFlow.displayName = \"ChartStatFlow\";\n",
      "type": "registry:component",
      "target": "components/charts/chart-stat-flow.tsx"
    },
    {
      "path": "src/charts/pie-context.tsx",
      "content": "\"use client\";\n\nimport type { Transition } from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  type RefObject,\n  useContext,\n  useMemo,\n} from \"react\";\n\n// CSS variable references for pie chart theming\nexport const pieCssVars = {\n  background: \"var(--chart-background)\",\n  foreground: \"var(--chart-foreground)\",\n  foregroundMuted: \"var(--chart-foreground-muted)\",\n  label: \"var(--chart-label)\",\n  // Default slice colors from chart palette\n  slice1: \"var(--chart-1)\",\n  slice2: \"var(--chart-2)\",\n  slice3: \"var(--chart-3)\",\n  slice4: \"var(--chart-4)\",\n  slice5: \"var(--chart-5)\",\n};\n\n// Default slice color palette\nexport const defaultPieColors = [\n  pieCssVars.slice1,\n  pieCssVars.slice2,\n  pieCssVars.slice3,\n  pieCssVars.slice4,\n  pieCssVars.slice5,\n];\n\nexport interface PieData {\n  /** Display label for the slice */\n  label: string;\n  /** Value for the slice (determines slice size relative to total) */\n  value: number;\n  /** Optional color override - falls back to palette */\n  color?: string;\n  /** Optional fill override for patterns/gradients (e.g., \"url(#patternId)\") */\n  fill?: string;\n}\n\n/** Arc data computed by visx Pie */\nexport interface PieArcData {\n  data: PieData;\n  index: number;\n  startAngle: number;\n  endAngle: number;\n  padAngle: number;\n  value: number;\n}\n\nexport interface PieHoverContextValue {\n  hoveredIndex: number | null;\n  setHoveredIndex: (index: number | null) => void;\n}\n\nexport interface PieStableContextValue {\n  // Data\n  data: PieData[];\n  arcs: PieArcData[];\n\n  // Dimensions\n  size: number;\n  center: number;\n  outerRadius: number;\n  innerRadius: number;\n  padAngle: number;\n  cornerRadius: number;\n\n  // Hover effect\n  hoverOffset: number;\n\n  // Animation state\n  animationKey: number;\n  isLoaded: boolean;\n  enterTransition?: Transition;\n  enterStaggerScale: number;\n\n  // Container ref for portals\n  containerRef: RefObject<HTMLDivElement | null>;\n\n  // Computed values\n  totalValue: number;\n\n  // Get color for a slice index\n  getColor: (index: number) => string;\n\n  // Get fill for a slice index (supports patterns/gradients)\n  getFill: (index: number) => string;\n\n  /**\n   * Studio geometry scrub — skip Motion path morphing and use plain SVG paths.\n   * @default false\n   */\n  geometryScrubbing: boolean;\n\n  /** Precomputed slice paths during geometry scrub (one per arc). */\n  scrubSlicePaths: readonly string[] | null;\n}\n\nexport type PieContextValue = PieStableContextValue & PieHoverContextValue;\n\nconst PieStableContext = createContext<PieStableContextValue | null>(null);\nconst PieHoverContext = createContext<PieHoverContextValue | null>(null);\n\nexport function PieProvider({\n  children,\n  value,\n}: {\n  children: ReactNode;\n  value: PieContextValue;\n}) {\n  const stable = useMemo<PieStableContextValue>(\n    () => ({\n      data: value.data,\n      arcs: value.arcs,\n      size: value.size,\n      center: value.center,\n      outerRadius: value.outerRadius,\n      innerRadius: value.innerRadius,\n      padAngle: value.padAngle,\n      cornerRadius: value.cornerRadius,\n      hoverOffset: value.hoverOffset,\n      animationKey: value.animationKey,\n      isLoaded: value.isLoaded,\n      enterTransition: value.enterTransition,\n      enterStaggerScale: value.enterStaggerScale,\n      containerRef: value.containerRef,\n      totalValue: value.totalValue,\n      getColor: value.getColor,\n      getFill: value.getFill,\n      geometryScrubbing: value.geometryScrubbing,\n      scrubSlicePaths: value.scrubSlicePaths,\n    }),\n    [\n      value.data,\n      value.arcs,\n      value.size,\n      value.center,\n      value.outerRadius,\n      value.innerRadius,\n      value.padAngle,\n      value.cornerRadius,\n      value.hoverOffset,\n      value.animationKey,\n      value.isLoaded,\n      value.enterTransition,\n      value.enterStaggerScale,\n      value.containerRef,\n      value.totalValue,\n      value.getColor,\n      value.getFill,\n      value.geometryScrubbing,\n      value.scrubSlicePaths,\n    ]\n  );\n\n  const hover = useMemo<PieHoverContextValue>(\n    () => ({\n      hoveredIndex: value.hoveredIndex,\n      setHoveredIndex: value.setHoveredIndex,\n    }),\n    [value.hoveredIndex, value.setHoveredIndex]\n  );\n\n  return (\n    <PieStableContext.Provider value={stable}>\n      <PieHoverContext.Provider value={hover}>\n        {children}\n      </PieHoverContext.Provider>\n    </PieStableContext.Provider>\n  );\n}\n\nexport function usePieStable(): PieStableContextValue {\n  const context = useContext(PieStableContext);\n  if (!context) {\n    throw new Error(\n      \"usePieStable must be used within a PieProvider. \" +\n        \"Make sure your component is wrapped in <PieChart>.\"\n    );\n  }\n  return context;\n}\n\nexport function usePieHover(): PieHoverContextValue {\n  const context = useContext(PieHoverContext);\n  if (!context) {\n    throw new Error(\n      \"usePieHover must be used within a PieProvider. \" +\n        \"Make sure your component is wrapped in <PieChart>.\"\n    );\n  }\n  return context;\n}\n\nexport function usePie(): PieContextValue {\n  return { ...usePieStable(), ...usePieHover() };\n}\n\nexport default PieStableContext;\n",
      "type": "registry:component",
      "target": "components/charts/pie-context.tsx"
    },
    {
      "path": "src/charts/pie-center-shell.tsx",
      "content": "\"use client\";\n\nimport { pie as d3Pie } from \"d3-shape\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { PieCenter, type PieCenterProps } from \"./pie-center\";\nimport {\n  defaultPieColors,\n  type PieArcData,\n  type PieContextValue,\n  type PieData,\n  PieProvider,\n} from \"./pie-context\";\n\nconst SHELL_HOVER_OFFSET = 10;\n\nexport type PieCenterShellProps = Omit<PieCenterProps, \"children\"> & {\n  /** Value shown with NumberFlow (same role as pie total when not hovering) */\n  centerValue: number;\n  /** Square reference size for pie context (matches `PieChart` `size`) */\n  contextSize: number;\n  /** Inner radius in px — must be > 0 so `PieCenter` renders */\n  innerRadiusPx: number;\n  /**\n   * When true (default), the first paint uses `0` then updates to `centerValue`\n   * on the next frame so NumberFlow can run an entrance transition. Subsequent\n   * `centerValue` updates animate as usual.\n   */\n  animateEntrance?: boolean;\n};\n\n/**\n * Renders {@link PieCenter} with a minimal {@link PieProvider} so you can reuse\n * the same center layout as a donut pie without mounting slices or a full {@link PieChart}.\n */\nexport function PieCenterShell({\n  centerValue,\n  contextSize,\n  innerRadiusPx,\n  animateEntrance = true,\n  ...pieCenterProps\n}: PieCenterShellProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const introStartedRef = useRef(false);\n\n  const [flowTotal, setFlowTotal] = useState(() =>\n    animateEntrance ? 0 : centerValue\n  );\n\n  useEffect(() => {\n    if (!animateEntrance) {\n      setFlowTotal(centerValue);\n      return;\n    }\n\n    if (!introStartedRef.current) {\n      introStartedRef.current = true;\n      setFlowTotal(0);\n      let innerRaf = 0;\n      const outerRaf = requestAnimationFrame(() => {\n        innerRaf = requestAnimationFrame(() => setFlowTotal(centerValue));\n      });\n      return () => {\n        cancelAnimationFrame(outerRaf);\n        cancelAnimationFrame(innerRaf);\n        introStartedRef.current = false;\n      };\n    }\n\n    setFlowTotal(centerValue);\n  }, [animateEntrance, centerValue]);\n\n  const data: PieData[] = useMemo(\n    () => [{ label: \"_pieCenterShell\", value: Math.max(flowTotal, 0) }],\n    [flowTotal]\n  );\n\n  const totalValue = flowTotal;\n\n  const arcs = useMemo((): PieArcData[] => {\n    const v = data[0]?.value ?? 0;\n    if (v > 0) {\n      const pieGenerator = d3Pie<PieData>()\n        .value((d) => d.value)\n        .startAngle(-Math.PI / 2)\n        .endAngle((3 * Math.PI) / 2)\n        .padAngle(0)\n        .sort(null);\n      const computed = pieGenerator(data);\n      return computed.map((arc, index) => ({\n        data: arc.data,\n        index,\n        startAngle: arc.startAngle,\n        endAngle: arc.endAngle,\n        padAngle: arc.padAngle,\n        value: arc.value,\n      })) as PieArcData[];\n    }\n    const d0 = data[0];\n    if (!d0) {\n      return [];\n    }\n    return [\n      {\n        data: d0,\n        index: 0,\n        startAngle: -Math.PI / 2,\n        endAngle: (3 * Math.PI) / 2,\n        padAngle: 0,\n        value: 0,\n      },\n    ];\n  }, [data]);\n\n  const getColor = useCallback((index: number) => {\n    return defaultPieColors[index % defaultPieColors.length] as string;\n  }, []);\n\n  const getFill = useCallback(\n    (index: number) => {\n      const item = data[index];\n      if (item?.fill) {\n        return item.fill;\n      }\n      return getColor(index);\n    },\n    [data, getColor]\n  );\n\n  const center = contextSize / 2;\n  const outerRadius = center - SHELL_HOVER_OFFSET;\n\n  const contextValue: PieContextValue = useMemo(\n    () => ({\n      data,\n      arcs,\n      size: contextSize,\n      center,\n      outerRadius,\n      innerRadius: innerRadiusPx,\n      padAngle: 0,\n      cornerRadius: 0,\n      hoverOffset: SHELL_HOVER_OFFSET,\n      hoveredIndex: null,\n      setHoveredIndex: () => undefined,\n      animationKey: 0,\n      isLoaded: true,\n      enterStaggerScale: 1,\n      containerRef,\n      totalValue,\n      getColor,\n      getFill,\n      geometryScrubbing: false,\n      scrubSlicePaths: null,\n    }),\n    [\n      data,\n      arcs,\n      contextSize,\n      center,\n      outerRadius,\n      innerRadiusPx,\n      totalValue,\n      getColor,\n      getFill,\n    ]\n  );\n\n  return (\n    <PieProvider value={contextValue}>\n      <PieCenter {...pieCenterProps} />\n    </PieProvider>\n  );\n}\n\nPieCenterShell.displayName = \"PieCenterShell\";\n",
      "type": "registry:component",
      "target": "components/charts/pie-center-shell.tsx"
    },
    {
      "path": "src/charts/pie-center.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  chartCenterContainerClassName,\n  chartCenterLabelClassName,\n  chartCenterValueClassName,\n} from \"./chart-center-typography\";\nimport {\n  ChartStatFlow,\n  type ChartStatFlowFormat,\n  defaultChartStatFlowFormat,\n} from \"./chart-stat-flow\";\nimport { usePieHover, usePieStable } from \"./pie-context\";\n\nexport interface PieCenterProps {\n  /** Label shown below the value. Default: \"Total\" when not hovering */\n  defaultLabel?: string;\n  /** Format options for NumberFlow. Default: standard notation */\n  formatOptions?: ChartStatFlowFormat;\n  /** Custom render function for complete control over center content */\n  children?: (props: {\n    value: number;\n    label: string;\n    isHovered: boolean;\n    data: { label: string; value: number; color?: string; fill?: string };\n  }) => ReactNode;\n  /** Additional class name for the container */\n  className?: string;\n  /** Class name for the value text. Scales with center size via container queries. */\n  valueClassName?: string;\n  /** Class name for the label text. Scales with center size via container queries. */\n  labelClassName?: string;\n  /** Prefix to show before the number (e.g., \"$\") */\n  prefix?: string;\n  /** Suffix to show after the number (e.g., \"%\") */\n  suffix?: string;\n}\n\n/**\n * PieCenter displays content in the center of a donut/pie chart.\n *\n * This component renders as pure HTML (not inside SVG foreignObject) to avoid\n * Safari's WebKit bug #23113 where HTML content with CSS transforms/opacity\n * inside foreignObject renders at incorrect positions.\n *\n * The parent PieChart uses CSS Grid stacking to overlay this HTML content\n * on top of the SVG slices.\n */\nexport function PieCenter({\n  defaultLabel = \"Total\",\n  formatOptions = defaultChartStatFlowFormat,\n  children,\n  className = \"\",\n  valueClassName = chartCenterValueClassName,\n  labelClassName = chartCenterLabelClassName,\n  prefix,\n  suffix,\n}: PieCenterProps) {\n  const { data, totalValue, innerRadius, geometryScrubbing } = usePieStable();\n  const { hoveredIndex } = usePieHover();\n\n  const effectiveHoveredIndex = geometryScrubbing ? null : hoveredIndex;\n  const hoveredData =\n    effectiveHoveredIndex === null ? null : data[effectiveHoveredIndex];\n  const displayValue = hoveredData ? hoveredData.value : totalValue;\n  const displayLabel = hoveredData ? hoveredData.label : defaultLabel;\n\n  // Calculate center area size based on inner radius\n  // Leave some padding so text doesn't touch the inner edge\n  const centerSize = innerRadius * 2 - 16;\n\n  // Don't render if there's no inner radius (solid pie, not donut)\n  if (innerRadius <= 0) {\n    return null;\n  }\n\n  // If custom render function is provided, use it\n  if (children && hoveredData) {\n    return (\n      <div\n        className={cn(\n          chartCenterContainerClassName,\n          \"flex items-center justify-center\",\n          className\n        )}\n        style={{ width: centerSize, height: centerSize }}\n      >\n        {children({\n          value: displayValue,\n          label: displayLabel,\n          isHovered: effectiveHoveredIndex !== null,\n          data: hoveredData,\n        })}\n      </div>\n    );\n  }\n\n  // Default center content with NumberFlow animations\n  // Now renders as pure HTML, avoiding Safari's foreignObject bugs\n  return (\n    <div\n      className={cn(\n        chartCenterContainerClassName,\n        \"flex flex-col items-center justify-center text-center\",\n        className\n      )}\n      style={{ width: centerSize, height: centerSize }}\n    >\n      <ChartStatFlow\n        formatOptions={formatOptions}\n        label={displayLabel}\n        labelClassName={labelClassName}\n        prefix={prefix}\n        suffix={suffix}\n        value={displayValue}\n        valueClassName={valueClassName}\n      />\n    </div>\n  );\n}\n\nPieCenter.displayName = \"PieCenter\";\n\nexport default PieCenter;\n",
      "type": "registry:component",
      "target": "components/charts/pie-center.tsx"
    },
    {
      "path": "src/charts/gauge.tsx",
      "content": "\"use client\";\n\nimport { ParentSize } from \"@visx/responsive\";\nimport { motion, type Transition, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useId, useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  type ChartStatFlowFormat,\n  defaultChartStatFlowFormat,\n} from \"./chart-stat-flow\";\nimport {\n  type GaugeLabelAlign,\n  GaugeLabelLayout,\n  type GaugeLabelPlacement,\n  GaugeLabelShell,\n} from \"./gauge-label-layout\";\nimport {\n  type ComputedNotch,\n  collectGaugeDefsElements,\n  createNotchPath,\n  DEFAULT_ACTIVE_FILL_OPACITY,\n  DEFAULT_ACTIVE_GRADIENT,\n  DEFAULT_INACTIVE_FILL_OPACITY,\n  DEFAULT_LINEAR_GAUGE_HEIGHT,\n  interpolateGaugeHex,\n  resolveGaugeActiveFill,\n  resolveGaugeBgFill,\n} from \"./notch-gauge-shared\";\nimport { PieCenterShell } from \"./pie-center-shell\";\n\nconst DEFAULT_NOTCH_ENTER_TRANSITION: Transition = {\n  type: \"spring\",\n  stiffness: 300,\n  damping: 20,\n};\n\nexport type GaugeOrientation = \"arc\" | \"linear\";\n\nexport interface GaugeProps {\n  /** Arc (default) or horizontal linear notch track */\n  orientation?: GaugeOrientation;\n  /** Fill level 0–100 */\n  value: number;\n  /** Number of notches */\n  totalNotches?: number;\n  /** Percentage of the track reserved for gaps between notches */\n  spacing?: number;\n  notchCornerRadius?: number;\n  /** `true` = rectangular notches; `false` = tapered toward center / midline */\n  uniformWidth?: boolean;\n  startAngle?: number;\n  endAngle?: number;\n  useGradient?: boolean;\n  activeGradient?: readonly [string, string];\n  inactiveGradient?: readonly [string, string];\n  /** Center statistic — omit to hide the label block */\n  centerValue?: number;\n  defaultLabel?: string;\n  prefix?: string;\n  suffix?: string;\n  formatOptions?: ChartStatFlowFormat;\n  /** Label position for `orientation=\"linear\"`. Arc gauges always overlay center. */\n  labelPlacement?: GaugeLabelPlacement;\n  /** Cross-axis alignment (start / center / end), same model as chart legend */\n  labelAlign?: GaugeLabelAlign;\n  inactiveFill?: string;\n  activeFill?: string;\n  inactiveFillOpacity?: number;\n  activeFillOpacity?: number;\n  children?: ReactNode;\n  className?: string;\n  width?: number;\n  height?: number;\n  minWidth?: number;\n  notchLengthPercent?: number;\n  /** Linear only — notch width as % of each slot (default 80) */\n  notchWidthPercent?: number;\n  /** Linear only — bar thickness in px when responsive (default 24) */\n  linearHeight?: number;\n  enterTransition?: Transition;\n  enterStaggerScale?: number;\n  /** Studio-only: static paths while scrubbing geometry controls */\n  geometryScrubbing?: boolean;\n}\n\ninterface GaugeInnerProps extends Omit<GaugeProps, \"className\" | \"minWidth\"> {\n  width: number;\n  height: number;\n}\n\nfunction GaugeNotchSvg({\n  notches,\n  width,\n  height,\n  notchCornerRadius,\n  cornerDepth,\n  geometryScrubbing,\n  notchTransition,\n  stagger,\n  defsChildren,\n  useThemePaletteGradient,\n  themeActiveGradientId,\n  resolvedInactiveFillOpacity,\n  resolvedActiveFillOpacity,\n  resolveBgFill,\n  resolveActiveFill,\n}: {\n  notches: ComputedNotch[];\n  width: number;\n  height: number;\n  notchCornerRadius: number;\n  cornerDepth: number;\n  geometryScrubbing: boolean;\n  notchTransition: Transition;\n  stagger: number;\n  defsChildren: ReactNode[];\n  useThemePaletteGradient: boolean;\n  themeActiveGradientId: string;\n  resolvedInactiveFillOpacity: number;\n  resolvedActiveFillOpacity: number;\n  resolveBgFill: (index: number) => string;\n  resolveActiveFill: (notch: ComputedNotch) => string;\n}) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"block w-full overflow-visible\"\n      height={height}\n      viewBox={`0 0 ${width} ${height}`}\n      width={width}\n    >\n      {defsChildren.length > 0 || useThemePaletteGradient ? (\n        <defs>\n          {useThemePaletteGradient ? (\n            <linearGradient\n              id={themeActiveGradientId}\n              x1=\"0%\"\n              x2=\"100%\"\n              y1=\"0%\"\n              y2=\"0%\"\n            >\n              <stop offset=\"0%\" stopColor=\"var(--chart-1)\" />\n              <stop offset=\"100%\" stopColor=\"var(--chart-5)\" />\n            </linearGradient>\n          ) : null}\n          {defsChildren}\n        </defs>\n      ) : null}\n      {notches.map((notch) => {\n        const pathD = createNotchPath(\n          notch.points,\n          notchCornerRadius,\n          cornerDepth\n        );\n        if (geometryScrubbing) {\n          return (\n            <path\n              d={pathD}\n              fill={resolveBgFill(notch.index)}\n              fillOpacity={resolvedInactiveFillOpacity}\n              key={`bg-${notch.index}`}\n            />\n          );\n        }\n        return (\n          <motion.path\n            animate={{ opacity: 1, scale: 1 }}\n            d={pathD}\n            fill={resolveBgFill(notch.index)}\n            fillOpacity={resolvedInactiveFillOpacity}\n            initial={{ opacity: 0, scale: 0 }}\n            key={`bg-${notch.index}`}\n            style={{\n              transformOrigin: `${notch.xCenter}px ${notch.yCenter}px`,\n            }}\n            transition={{\n              ...notchTransition,\n              delay: notch.index * 0.015 * stagger,\n            }}\n          />\n        );\n      })}\n      {notches\n        .filter((n) => n.isActive)\n        .map((notch) => {\n          const pathD = createNotchPath(\n            notch.points,\n            notchCornerRadius,\n            cornerDepth\n          );\n          if (geometryScrubbing) {\n            return (\n              <path\n                d={pathD}\n                fill={resolveActiveFill(notch)}\n                fillOpacity={resolvedActiveFillOpacity}\n                key={`active-${notch.index}`}\n              />\n            );\n          }\n          return (\n            <motion.path\n              animate={{ opacity: 1, scale: 1 }}\n              d={pathD}\n              fill={resolveActiveFill(notch)}\n              fillOpacity={resolvedActiveFillOpacity}\n              initial={{ opacity: 0, scale: 0 }}\n              key={`active-${notch.index}`}\n              style={{\n                transformOrigin: `${notch.xCenter}px ${notch.yCenter}px`,\n              }}\n              transition={{\n                ...notchTransition,\n                delay: (0.3 + notch.index * 0.02) * stagger,\n              }}\n            />\n          );\n        })}\n    </svg>\n  );\n}\n\nfunction useGaugeFillState(props: GaugeInnerProps) {\n  const {\n    useGradient = false,\n    activeGradient,\n    inactiveGradient,\n    inactiveFill,\n    activeFill,\n    inactiveFillOpacity,\n    activeFillOpacity,\n    children,\n    totalNotches = 40,\n  } = props;\n\n  const themeActiveGradientId = `gauge-theme-active-${useId().replace(/:/g, \"\")}`;\n  const defsChildren = useMemo(\n    () => collectGaugeDefsElements(children),\n    [children]\n  );\n\n  const hasCustomInactive =\n    inactiveFill !== undefined && inactiveFill.length > 0;\n  const hasCustomActive = activeFill !== undefined && activeFill.length > 0;\n\n  const activeGrad0 = activeGradient?.[0] ?? DEFAULT_ACTIVE_GRADIENT[0];\n  const activeGrad1 = activeGradient?.[1] ?? DEFAULT_ACTIVE_GRADIENT[1];\n  const inactiveGrad0 = inactiveGradient?.[0] ?? activeGrad0;\n  const inactiveGrad1 = inactiveGradient?.[1] ?? activeGrad1;\n  const useThemePaletteGradient = useGradient && activeGradient === undefined;\n\n  return {\n    themeActiveGradientId,\n    defsChildren,\n    hasCustomInactive,\n    hasCustomActive,\n    activeGrad0,\n    activeGrad1,\n    inactiveGrad0,\n    inactiveGrad1,\n    useThemePaletteGradient,\n    resolvedActiveFillOpacity: activeFillOpacity ?? DEFAULT_ACTIVE_FILL_OPACITY,\n    resolvedInactiveFillOpacity:\n      inactiveFillOpacity ?? DEFAULT_INACTIVE_FILL_OPACITY,\n    totalNotches,\n  };\n}\n\nfunction GaugeArcInner(props: GaugeInnerProps) {\n  const {\n    value,\n    totalNotches = 40,\n    spacing = 25,\n    notchCornerRadius = 0,\n    uniformWidth = false,\n    width,\n    height,\n    startAngle = 135,\n    endAngle = 405,\n    useGradient = false,\n    centerValue,\n    defaultLabel = \"Total\",\n    prefix,\n    suffix,\n    formatOptions = defaultChartStatFlowFormat,\n    inactiveFill,\n    activeFill,\n    notchLengthPercent = 100,\n    enterTransition,\n    enterStaggerScale = 1,\n  } = props;\n\n  const prefersReducedMotion = useReducedMotion();\n  const fillState = useGaugeFillState(props);\n\n  const notchTransition: Transition = prefersReducedMotion\n    ? { duration: 0 }\n    : (enterTransition ?? DEFAULT_NOTCH_ENTER_TRANSITION);\n  const stagger = Math.max(0.25, Math.min(2.5, enterStaggerScale));\n\n  const size = Math.min(width, height);\n  const centerX = width / 2;\n  const centerY = height / 2;\n  const outerRadius = size * 0.42;\n  const innerRadiusBase = size * 0.28;\n  const defaultRadialDepth = outerRadius - innerRadiusBase;\n  const depthFactor = Math.min(100, Math.max(5, notchLengthPercent)) / 100;\n  const notchLength = defaultRadialDepth * depthFactor;\n  const innerRadius = outerRadius - notchLength;\n\n  const activeNotches = Math.round((value / 100) * totalNotches);\n  const totalAngle = endAngle - startAngle;\n  const availableAngle = totalAngle * (1 - spacing / 100);\n  const notchAngle = totalNotches > 0 ? availableAngle / totalNotches : 0;\n  const gapDen = totalNotches - 1 > 0 ? totalNotches - 1 : 1;\n  const gapAngle = (totalAngle * (spacing / 100)) / gapDen;\n\n  const notches = useMemo(() => {\n    return Array.from({ length: totalNotches }, (_, i) => {\n      const angle = startAngle + i * (notchAngle + gapAngle) + notchAngle / 2;\n      const radians = (angle * Math.PI) / 180;\n      const arcNotchWidth = notchAngle * 0.8;\n      const halfWidth = (arcNotchWidth * Math.PI) / 180 / 2;\n\n      const x1 = centerX + Math.cos(radians - halfWidth) * outerRadius;\n      const y1 = centerY + Math.sin(radians - halfWidth) * outerRadius;\n      const x2 = centerX + Math.cos(radians + halfWidth) * outerRadius;\n      const y2 = centerY + Math.sin(radians + halfWidth) * outerRadius;\n\n      let x3: number;\n      let y3: number;\n      let x4: number;\n      let y4: number;\n\n      if (uniformWidth) {\n        const perpX = Math.cos(radians);\n        const perpY = Math.sin(radians);\n        x3 = x2 - perpX * notchLength;\n        y3 = y2 - perpY * notchLength;\n        x4 = x1 - perpX * notchLength;\n        y4 = y1 - perpY * notchLength;\n      } else {\n        x3 = centerX + Math.cos(radians + halfWidth) * innerRadius;\n        y3 = centerY + Math.sin(radians + halfWidth) * innerRadius;\n        x4 = centerX + Math.cos(radians - halfWidth) * innerRadius;\n        y4 = centerY + Math.sin(radians - halfWidth) * innerRadius;\n      }\n\n      const denom = totalNotches > 1 ? totalNotches - 1 : 1;\n      const gradientColor =\n        useGradient && !fillState.useThemePaletteGradient\n          ? interpolateGaugeHex(\n              fillState.activeGrad0,\n              fillState.activeGrad1,\n              i / denom\n            )\n          : \"var(--chart-1)\";\n\n      return {\n        index: i,\n        points: { x1, y1, x2, y2, x3, y3, x4, y4 },\n        isActive: i < activeNotches,\n        gradientColor,\n        xCenter: centerX,\n        yCenter: centerY,\n      };\n    });\n  }, [\n    totalNotches,\n    notchAngle,\n    gapAngle,\n    centerX,\n    centerY,\n    outerRadius,\n    innerRadius,\n    activeNotches,\n    startAngle,\n    uniformWidth,\n    notchLength,\n    useGradient,\n    fillState.useThemePaletteGradient,\n    fillState.activeGrad0,\n    fillState.activeGrad1,\n  ]);\n\n  const resolveBgFill = (notchIndex: number) =>\n    resolveGaugeBgFill({\n      notchIndex,\n      totalNotches,\n      hasCustomInactive: fillState.hasCustomInactive,\n      inactiveFill,\n      useThemePaletteGradient: fillState.useThemePaletteGradient,\n      useGradient,\n      inactiveGrad0: fillState.inactiveGrad0,\n      inactiveGrad1: fillState.inactiveGrad1,\n      arcTrackFill: \"var(--border)\",\n      linearTrackFill: \"var(--chart-background)\",\n      linearMode: false,\n    });\n\n  const resolveActiveFill = (notch: ComputedNotch) =>\n    resolveGaugeActiveFill({\n      notch,\n      hasCustomActive: fillState.hasCustomActive,\n      activeFill,\n      useThemePaletteGradient: fillState.useThemePaletteGradient,\n      themeActiveGradientId: fillState.themeActiveGradientId,\n      useGradient,\n      activeFillSolid: \"var(--chart-1)\",\n    });\n\n  const showCenter = centerValue != null;\n\n  return (\n    <div className=\"relative w-full\" style={{ height, width }}>\n      <GaugeNotchSvg\n        cornerDepth={notchLength}\n        defsChildren={fillState.defsChildren}\n        geometryScrubbing={false}\n        height={height}\n        notchCornerRadius={notchCornerRadius}\n        notches={notches}\n        notchTransition={notchTransition}\n        resolveActiveFill={resolveActiveFill}\n        resolveBgFill={resolveBgFill}\n        resolvedActiveFillOpacity={fillState.resolvedActiveFillOpacity}\n        resolvedInactiveFillOpacity={fillState.resolvedInactiveFillOpacity}\n        stagger={stagger}\n        themeActiveGradientId={fillState.themeActiveGradientId}\n        useThemePaletteGradient={fillState.useThemePaletteGradient}\n        width={width}\n      />\n      {showCenter ? (\n        <div\n          className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center\"\n          style={{ paddingTop: size * 0.08 }}\n        >\n          <PieCenterShell\n            centerValue={centerValue}\n            contextSize={size}\n            defaultLabel={defaultLabel}\n            formatOptions={formatOptions}\n            innerRadiusPx={Math.max(size * 0.2, 52)}\n            prefix={prefix}\n            suffix={suffix}\n          />\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction GaugeLinearInner(props: GaugeInnerProps) {\n  const {\n    value,\n    totalNotches = 40,\n    spacing = 25,\n    notchCornerRadius = 0,\n    uniformWidth = true,\n    width,\n    height,\n    useGradient = false,\n    centerValue,\n    defaultLabel = \"Total\",\n    prefix,\n    suffix,\n    formatOptions = defaultChartStatFlowFormat,\n    labelPlacement = \"top\",\n    labelAlign = \"start\",\n    inactiveFill,\n    activeFill,\n    notchLengthPercent = 100,\n    notchWidthPercent = 80,\n    enterTransition,\n    enterStaggerScale = 1,\n    geometryScrubbing = false,\n  } = props;\n\n  const prefersReducedMotion = useReducedMotion();\n  const fillState = useGaugeFillState(props);\n\n  const notchTransition: Transition = prefersReducedMotion\n    ? { duration: 0 }\n    : (enterTransition ?? DEFAULT_NOTCH_ENTER_TRANSITION);\n  const stagger = Math.max(0.25, Math.min(2.5, enterStaggerScale));\n\n  const centerY = height / 2;\n  const depthFactor = Math.min(100, Math.max(5, notchLengthPercent)) / 100;\n  const outerOffset = (height / 2) * depthFactor;\n  const taperRatio = 28 / 42;\n  const innerOffset = uniformWidth ? outerOffset : outerOffset * taperRatio;\n  const notchDepth = uniformWidth ? outerOffset * 2 : outerOffset - innerOffset;\n  const cornerVerticalDepth = uniformWidth ? notchDepth : outerOffset * 2;\n  const widthFactor = Math.min(100, Math.max(10, notchWidthPercent)) / 100;\n\n  const activeNotches = Math.round((value / 100) * totalNotches);\n  const availableWidth = width * (1 - spacing / 100);\n  const slotWidth = totalNotches > 0 ? availableWidth / totalNotches : 0;\n  const gapDen = totalNotches - 1 > 0 ? totalNotches - 1 : 1;\n  const gapWidth = (width * (spacing / 100)) / gapDen;\n\n  const notches = useMemo(() => {\n    return Array.from({ length: totalNotches }, (_, i) => {\n      const xCenter = i * (slotWidth + gapWidth) + slotWidth / 2;\n      const halfWidth = (slotWidth * widthFactor) / 2;\n\n      let x1: number;\n      let y1: number;\n      let x2: number;\n      let y2: number;\n      let x3: number;\n      let y3: number;\n      let x4: number;\n      let y4: number;\n\n      if (uniformWidth) {\n        const halfHeight = notchDepth / 2;\n        x1 = xCenter - halfWidth;\n        y1 = centerY - halfHeight;\n        x2 = xCenter + halfWidth;\n        y2 = centerY - halfHeight;\n        x3 = xCenter + halfWidth;\n        y3 = centerY + halfHeight;\n        x4 = xCenter - halfWidth;\n        y4 = centerY + halfHeight;\n      } else {\n        x1 = xCenter - halfWidth;\n        y1 = centerY - outerOffset;\n        x2 = xCenter + halfWidth;\n        y2 = centerY - outerOffset;\n        const innerHalfWidth = halfWidth * (innerOffset / outerOffset);\n        x3 = xCenter + innerHalfWidth;\n        y3 = centerY + outerOffset;\n        x4 = xCenter - innerHalfWidth;\n        y4 = centerY + outerOffset;\n      }\n\n      const denom = totalNotches > 1 ? totalNotches - 1 : 1;\n      const gradientColor =\n        useGradient && !fillState.useThemePaletteGradient\n          ? interpolateGaugeHex(\n              fillState.activeGrad0,\n              fillState.activeGrad1,\n              i / denom\n            )\n          : \"var(--chart-1)\";\n\n      return {\n        index: i,\n        points: { x1, y1, x2, y2, x3, y3, x4, y4 },\n        isActive: i < activeNotches,\n        gradientColor,\n        xCenter,\n        yCenter: centerY,\n      };\n    });\n  }, [\n    totalNotches,\n    slotWidth,\n    gapWidth,\n    centerY,\n    outerOffset,\n    innerOffset,\n    activeNotches,\n    uniformWidth,\n    notchDepth,\n    widthFactor,\n    useGradient,\n    fillState.useThemePaletteGradient,\n    fillState.activeGrad0,\n    fillState.activeGrad1,\n  ]);\n\n  const resolveBgFill = (notchIndex: number) =>\n    resolveGaugeBgFill({\n      notchIndex,\n      totalNotches,\n      hasCustomInactive: fillState.hasCustomInactive,\n      inactiveFill,\n      useThemePaletteGradient: fillState.useThemePaletteGradient,\n      useGradient,\n      inactiveGrad0: fillState.inactiveGrad0,\n      inactiveGrad1: fillState.inactiveGrad1,\n      arcTrackFill: \"var(--border)\",\n      linearTrackFill: \"var(--chart-background)\",\n      linearMode: true,\n    });\n\n  const resolveActiveFill = (notch: ComputedNotch) =>\n    resolveGaugeActiveFill({\n      notch,\n      hasCustomActive: fillState.hasCustomActive,\n      activeFill,\n      useThemePaletteGradient: fillState.useThemePaletteGradient,\n      themeActiveGradientId: fillState.themeActiveGradientId,\n      useGradient,\n      activeFillSolid: \"var(--chart-1)\",\n    });\n\n  const label =\n    centerValue == null ? null : (\n      <GaugeLabelShell\n        align={labelAlign}\n        centerValue={centerValue}\n        defaultLabel={defaultLabel}\n        formatOptions={formatOptions}\n        prefix={prefix}\n        suffix={suffix}\n      />\n    );\n\n  const track = (\n    <div className=\"relative w-full\" style={{ height, width }}>\n      <GaugeNotchSvg\n        cornerDepth={cornerVerticalDepth}\n        defsChildren={fillState.defsChildren}\n        geometryScrubbing={geometryScrubbing}\n        height={height}\n        notchCornerRadius={notchCornerRadius}\n        notches={notches}\n        notchTransition={notchTransition}\n        resolveActiveFill={resolveActiveFill}\n        resolveBgFill={resolveBgFill}\n        resolvedActiveFillOpacity={fillState.resolvedActiveFillOpacity}\n        resolvedInactiveFillOpacity={fillState.resolvedInactiveFillOpacity}\n        stagger={stagger}\n        themeActiveGradientId={fillState.themeActiveGradientId}\n        useThemePaletteGradient={fillState.useThemePaletteGradient}\n        width={width}\n      />\n    </div>\n  );\n\n  return (\n    <GaugeLabelLayout\n      align={labelAlign}\n      label={label}\n      placement={labelPlacement}\n    >\n      {track}\n    </GaugeLabelLayout>\n  );\n}\n\nfunction GaugeInner(props: GaugeInnerProps) {\n  if (props.orientation === \"linear\") {\n    return <GaugeLinearInner {...props} />;\n  }\n  return <GaugeArcInner {...props} />;\n}\n\nexport function Gauge({\n  width: widthProp,\n  height: heightProp,\n  className,\n  minWidth,\n  orientation = \"arc\",\n  linearHeight,\n  ...props\n}: GaugeProps) {\n  const isLinear = orientation === \"linear\";\n  const resolvedMinWidth = minWidth ?? (isLinear ? 200 : 300);\n  const resolvedLinearHeight = linearHeight ?? DEFAULT_LINEAR_GAUGE_HEIGHT;\n\n  if (isLinear) {\n    if (widthProp != null) {\n      return (\n        <div\n          className={cn(\"relative w-full max-w-full\", className)}\n          style={{ width: widthProp }}\n        >\n          <GaugeInner\n            height={heightProp ?? resolvedLinearHeight}\n            orientation=\"linear\"\n            width={widthProp}\n            {...props}\n          />\n        </div>\n      );\n    }\n\n    return (\n      <div className={cn(\"relative w-full min-w-0 max-w-full\", className)}>\n        <div className=\"w-full min-w-0\" style={{ minWidth: resolvedMinWidth }}>\n          <ParentSize debounceTime={10}>\n            {({ width }) =>\n              width > 0 ? (\n                <GaugeInner\n                  height={resolvedLinearHeight}\n                  orientation=\"linear\"\n                  width={width}\n                  {...props}\n                />\n              ) : null\n            }\n          </ParentSize>\n        </div>\n      </div>\n    );\n  }\n\n  if (widthProp != null && heightProp != null) {\n    return (\n      <div className={cn(\"relative inline-flex max-w-full\", className)}>\n        <GaugeInner\n          height={heightProp}\n          orientation=\"arc\"\n          width={widthProp}\n          {...props}\n        />\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={cn(\"relative w-full max-w-full\", className)}\n      style={{ minWidth: resolvedMinWidth }}\n    >\n      <div className=\"mx-auto aspect-[21/16] w-full max-w-[560px]\">\n        <ParentSize debounceTime={10}>\n          {({ width, height }) =>\n            width > 0 && height > 0 ? (\n              <GaugeInner\n                height={height}\n                orientation=\"arc\"\n                width={width}\n                {...props}\n              />\n            ) : null\n          }\n        </ParentSize>\n      </div>\n    </div>\n  );\n}\n\nGauge.displayName = \"Gauge\";\n",
      "type": "registry:component",
      "target": "components/charts/gauge.tsx"
    },
    {
      "path": "src/charts/notch-gauge-shared.ts",
      "content": "import {\n  Children,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\n\nexport const DEFAULT_ACTIVE_GRADIENT: readonly [string, string] = [\n  \"#bef264\",\n  \"#10b981\",\n];\n\nexport const DEFAULT_ACTIVE_FILL_OPACITY = 1;\nexport const DEFAULT_INACTIVE_FILL_OPACITY = 0.8;\nexport const DEFAULT_LINEAR_GAUGE_HEIGHT = 24;\n\nexport interface NotchPoint {\n  x1: number;\n  y1: number;\n  x2: number;\n  y2: number;\n  x3: number;\n  y3: number;\n  x4: number;\n  y4: number;\n}\n\nexport interface ComputedNotch {\n  index: number;\n  points: NotchPoint;\n  isActive: boolean;\n  gradientColor: string;\n  xCenter: number;\n  yCenter: number;\n}\n\nfunction isDefsComponent(child: ReactElement): boolean {\n  const typeLabel =\n    (child.type as { displayName?: string })?.displayName ||\n    (child.type as { name?: string })?.name ||\n    \"\";\n  return (\n    typeLabel.includes(\"Gradient\") ||\n    typeLabel.includes(\"Pattern\") ||\n    typeLabel === \"LinearGradient\" ||\n    typeLabel === \"RadialGradient\" ||\n    typeLabel === \"Lines\" ||\n    typeLabel === \"PatternLines\" ||\n    typeLabel === \"Circles\" ||\n    typeLabel === \"Hexagons\" ||\n    typeLabel === \"Waves\"\n  );\n}\n\nexport function collectGaugeDefsElements(nodes: ReactNode): ReactElement[] {\n  const out: ReactElement[] = [];\n  Children.forEach(nodes, (child) => {\n    if (!isValidElement(child)) {\n      return;\n    }\n    if (child.type === Fragment) {\n      out.push(\n        ...collectGaugeDefsElements(\n          (child.props as { children?: ReactNode }).children\n        )\n      );\n      return;\n    }\n    if (isDefsComponent(child)) {\n      out.push(child);\n    }\n  });\n  return out;\n}\n\nexport function interpolateGaugeHex(\n  color1: string,\n  color2: string,\n  factor: number\n): string {\n  const hex = (c: string) => Number.parseInt(c, 16);\n  const r1 = hex(color1.slice(1, 3));\n  const g1 = hex(color1.slice(3, 5));\n  const b1 = hex(color1.slice(5, 7));\n  const r2 = hex(color2.slice(1, 3));\n  const g2 = hex(color2.slice(3, 5));\n  const b2 = hex(color2.slice(5, 7));\n\n  const r = Math.round(r1 + (r2 - r1) * factor);\n  const g = Math.round(g1 + (g2 - g1) * factor);\n  const b = Math.round(b1 + (b2 - b1) * factor);\n\n  return `#${r.toString(16).padStart(2, \"0\")}${g.toString(16).padStart(2, \"0\")}${b.toString(16).padStart(2, \"0\")}`;\n}\n\nexport function createNotchPath(\n  points: NotchPoint,\n  cornerRadiusPx: number,\n  verticalDepth: number\n): string {\n  const { x1, y1, x2, y2, x3, y3, x4, y4 } = points;\n\n  const lerp = (a: number, b: number, t: number) => a + (b - a) * t;\n  const dist = (ax: number, ay: number, bx: number, by: number) =>\n    Math.hypot(bx - ax, by - ay);\n\n  const d12 = dist(x1, y1, x2, y2);\n  const d23 = dist(x2, y2, x3, y3);\n  const d34 = dist(x3, y3, x4, y4);\n  const d41 = dist(x4, y4, x1, y1);\n\n  if (cornerRadiusPx <= 0) {\n    return `M ${x1} ${y1} L ${x2} ${y2} L ${x3} ${y3} L ${x4} ${y4} Z`;\n  }\n\n  const minEdge = Math.min(d12, d23, d34, d41);\n  const cr = Math.min(\n    cornerRadiusPx,\n    verticalDepth * 0.48,\n    d12 * 0.49,\n    d23 * 0.49,\n    d34 * 0.49,\n    d41 * 0.49,\n    minEdge * 0.49\n  );\n\n  const r1 = Math.min(cr / d12, 0.49);\n  const r2 = Math.min(cr / d23, 0.49);\n  const r3 = Math.min(cr / d34, 0.49);\n  const r4 = Math.min(cr / d41, 0.49);\n\n  const p1a = { x: lerp(x1, x4, r4), y: lerp(y1, y4, r4) };\n  const p1b = { x: lerp(x1, x2, r1), y: lerp(y1, y2, r1) };\n  const p2a = { x: lerp(x2, x1, r1), y: lerp(y2, y1, r1) };\n  const p2b = { x: lerp(x2, x3, r2), y: lerp(y2, y3, r2) };\n  const p3a = { x: lerp(x3, x2, r2), y: lerp(y3, y2, r2) };\n  const p3b = { x: lerp(x3, x4, r3), y: lerp(y3, y4, r3) };\n  const p4a = { x: lerp(x4, x3, r3), y: lerp(y4, y3, r3) };\n  const p4b = { x: lerp(x4, x1, r4), y: lerp(y4, y1, r4) };\n\n  return `M ${p1a.x} ${p1a.y} Q ${x1} ${y1} ${p1b.x} ${p1b.y} L ${p2a.x} ${p2a.y} Q ${x2} ${y2} ${p2b.x} ${p2b.y} L ${p3a.x} ${p3a.y} Q ${x3} ${y3} ${p3b.x} ${p3b.y} L ${p4a.x} ${p4a.y} Q ${x4} ${y4} ${p4b.x} ${p4b.y} Z`;\n}\n\nexport function resolveGaugeBgFill(options: {\n  notchIndex: number;\n  totalNotches: number;\n  hasCustomInactive: boolean;\n  inactiveFill?: string;\n  useThemePaletteGradient: boolean;\n  useGradient: boolean;\n  inactiveGrad0: string;\n  inactiveGrad1: string;\n  arcTrackFill: string;\n  linearTrackFill: string;\n  linearMode: boolean;\n}): string {\n  const {\n    notchIndex,\n    totalNotches,\n    hasCustomInactive,\n    inactiveFill,\n    useThemePaletteGradient,\n    useGradient,\n    inactiveGrad0,\n    inactiveGrad1,\n    arcTrackFill,\n    linearTrackFill,\n    linearMode,\n  } = options;\n\n  if (hasCustomInactive) {\n    return inactiveFill as string;\n  }\n  if (useThemePaletteGradient) {\n    return linearMode ? \"var(--chart-1)\" : arcTrackFill;\n  }\n  if (useGradient) {\n    const denom = totalNotches > 1 ? totalNotches - 1 : 1;\n    return interpolateGaugeHex(\n      inactiveGrad0,\n      inactiveGrad1,\n      notchIndex / denom\n    );\n  }\n  return linearMode ? linearTrackFill : arcTrackFill;\n}\n\nexport function resolveGaugeActiveFill(options: {\n  notch: ComputedNotch;\n  hasCustomActive: boolean;\n  activeFill?: string;\n  useThemePaletteGradient: boolean;\n  themeActiveGradientId: string;\n  useGradient: boolean;\n  activeFillSolid: string;\n}): string {\n  const {\n    notch,\n    hasCustomActive,\n    activeFill,\n    useThemePaletteGradient,\n    themeActiveGradientId,\n    useGradient,\n    activeFillSolid,\n  } = options;\n\n  if (hasCustomActive) {\n    return activeFill as string;\n  }\n  if (useThemePaletteGradient) {\n    return `url(#${themeActiveGradientId})`;\n  }\n  if (useGradient) {\n    return notch.gradientColor;\n  }\n  return activeFillSolid;\n}\n",
      "type": "registry:component",
      "target": "components/charts/notch-gauge-shared.ts"
    },
    {
      "path": "src/charts/gauge-label-layout.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  chartCenterContainerClassName,\n  chartCenterLabelClassName,\n  chartCenterValueClassName,\n} from \"./chart-center-typography\";\nimport {\n  ChartStatFlow,\n  type ChartStatFlowFormat,\n  defaultChartStatFlowFormat,\n} from \"./chart-stat-flow\";\n\nexport type GaugeLabelPlacement = \"top\" | \"bottom\" | \"left\" | \"right\";\nexport type GaugeLabelAlign = \"start\" | \"center\" | \"end\";\n\nexport interface GaugeLabelShellProps {\n  centerValue: number;\n  defaultLabel?: string;\n  prefix?: string;\n  suffix?: string;\n  formatOptions?: ChartStatFlowFormat;\n  align?: GaugeLabelAlign;\n  className?: string;\n}\n\nconst labelAlignClass: Record<GaugeLabelAlign, string> = {\n  start: \"items-start text-left\",\n  center: \"items-center text-center\",\n  end: \"items-end text-right\",\n};\n\nexport function GaugeLabelShell({\n  centerValue,\n  defaultLabel = \"Total\",\n  prefix,\n  suffix,\n  formatOptions = defaultChartStatFlowFormat,\n  align = \"center\",\n  className,\n}: GaugeLabelShellProps) {\n  return (\n    <div\n      className={cn(\n        chartCenterContainerClassName,\n        \"flex min-w-0 flex-col\",\n        labelAlignClass[align],\n        className\n      )}\n    >\n      <ChartStatFlow\n        formatOptions={formatOptions}\n        label={defaultLabel}\n        labelClassName={cn(\n          chartCenterLabelClassName,\n          \"text-[length:var(--chart-foreground-muted)]\"\n        )}\n        prefix={prefix}\n        suffix={suffix}\n        value={centerValue}\n        valueClassName={cn(\n          chartCenterValueClassName,\n          \"text-[length:var(--chart-foreground)]\"\n        )}\n      />\n    </div>\n  );\n}\n\nconst crossAxisSelf: Record<GaugeLabelAlign, string> = {\n  start: \"self-start\",\n  center: \"self-center\",\n  end: \"self-end\",\n};\n\nconst crossAxisAlign: Record<GaugeLabelAlign, string> = {\n  start: \"items-start\",\n  center: \"items-center\",\n  end: \"items-end\",\n};\n\nconst inlineAxisAlign: Record<GaugeLabelAlign, string> = {\n  start: \"justify-start\",\n  center: \"justify-center\",\n  end: \"justify-end\",\n};\n\nexport function GaugeLabelLayout({\n  placement,\n  align,\n  label,\n  children,\n  className,\n}: {\n  placement: GaugeLabelPlacement;\n  align: GaugeLabelAlign;\n  label: ReactNode | null;\n  children: ReactNode;\n  className?: string;\n}) {\n  if (!label) {\n    return <div className={cn(\"w-full min-w-0\", className)}>{children}</div>;\n  }\n\n  if (placement === \"top\") {\n    return (\n      <div\n        className={cn(\n          \"flex w-full min-w-0 flex-col gap-3\",\n          crossAxisAlign[align],\n          className\n        )}\n      >\n        <div className={crossAxisSelf[align]}>{label}</div>\n        <div className=\"w-full min-w-0\">{children}</div>\n      </div>\n    );\n  }\n\n  if (placement === \"bottom\") {\n    return (\n      <div\n        className={cn(\n          \"flex w-full min-w-0 flex-col gap-3\",\n          crossAxisAlign[align],\n          className\n        )}\n      >\n        <div className=\"w-full min-w-0\">{children}</div>\n        <div className={crossAxisSelf[align]}>{label}</div>\n      </div>\n    );\n  }\n\n  if (placement === \"left\") {\n    return (\n      <div\n        className={cn(\n          \"flex w-full min-w-0 items-center gap-4\",\n          inlineAxisAlign[align],\n          className\n        )}\n      >\n        <div className=\"shrink-0\">{label}</div>\n        <div className=\"min-w-0 flex-1\">{children}</div>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full min-w-0 items-center gap-4\",\n        inlineAxisAlign[align],\n        className\n      )}\n    >\n      <div className=\"min-w-0 flex-1\">{children}</div>\n      <div className=\"shrink-0\">{label}</div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/gauge-label-layout.tsx"
    },
    {
      "path": "src/charts/chart-center-typography.ts",
      "content": "/**\n * Fluid typography for pie / ring / gauge center labels.\n *\n * Uses CSS container query units (`cqw`) so values scale with the center\n * hole — not the viewport — which keeps stat text readable on small charts.\n */\nexport const chartCenterContainerClassName =\n  \"@container/chart-center size-full min-w-0\";\n\n/** Primary stat — ~22% of center width, clamped between text-sm and text-3xl. */\nexport const chartCenterValueClassName =\n  \"font-bold tabular-nums leading-none text-[clamp(0.75rem,22cqw,1.875rem)]\";\n\n/** Supporting label — ~9% of center width, clamped between 10px and text-xs. */\nexport const chartCenterLabelClassName =\n  \"max-w-full truncate leading-tight text-[clamp(0.625rem,9cqw,0.75rem)]\";\n",
      "type": "registry:component",
      "target": "components/charts/chart-center-typography.ts"
    }
  ]
}