{
  "name": "gauge",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "files": [
    {
      "path": "ui/gauge.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst GAUGE_NAME = \"Gauge\";\nconst INDICATOR_NAME = \"GaugeIndicator\";\nconst TRACK_NAME = \"GaugeTrack\";\nconst RANGE_NAME = \"GaugeRange\";\nconst VALUE_TEXT_NAME = \"GaugeValueText\";\nconst LABEL_NAME = \"GaugeLabel\";\n\nconst DEFAULT_MAX = 100;\nconst DEFAULT_START_ANGLE = 0;\nconst DEFAULT_END_ANGLE = 360;\n\ntype GaugeState = \"indeterminate\" | \"complete\" | \"loading\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ninterface PathProps extends React.ComponentProps<\"path\"> {}\n\nfunction getGaugeState(\n  value: number | undefined | null,\n  maxValue: number,\n): GaugeState {\n  return value == null\n    ? \"indeterminate\"\n    : value === maxValue\n      ? \"complete\"\n      : \"loading\";\n}\n\nfunction getIsValidNumber(value: unknown): value is number {\n  return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction getIsValidMaxNumber(max: unknown): max is number {\n  return getIsValidNumber(max) && max > 0;\n}\n\nfunction getIsValidValueNumber(\n  value: unknown,\n  min: number,\n  max: number,\n): value is number {\n  return getIsValidNumber(value) && value <= max && value >= min;\n}\n\nfunction getDefaultValueText(value: number, min: number, max: number): string {\n  const percentage = max === min ? 100 : ((value - min) / (max - min)) * 100;\n  return Math.round(percentage).toString();\n}\n\nfunction getInvalidValueError(\n  propValue: string,\n  componentName: string,\n): string {\n  return `Invalid prop \\`value\\` of value \\`${propValue}\\` supplied to \\`${componentName}\\`. The \\`value\\` prop must be a number between \\`min\\` and \\`max\\` (inclusive), or \\`null\\`/\\`undefined\\` for indeterminate state. The value will be clamped to the valid range.`;\n}\n\nfunction getInvalidMaxError(propValue: string, componentName: string): string {\n  return `Invalid prop \\`max\\` of value \\`${propValue}\\` supplied to \\`${componentName}\\`. Only numbers greater than 0 are valid. Defaulting to ${DEFAULT_MAX}.`;\n}\n\nfunction getNormalizedAngle(angle: number) {\n  return ((angle % 360) + 360) % 360;\n}\n\nfunction polarToCartesian(\n  centerX: number,\n  centerY: number,\n  radius: number,\n  angleInDegrees: number,\n) {\n  const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;\n  return {\n    x: centerX + radius * Math.cos(angleInRadians),\n    y: centerY + radius * Math.sin(angleInRadians),\n  };\n}\n\nfunction describeArc(\n  x: number,\n  y: number,\n  radius: number,\n  startAngle: number,\n  endAngle: number,\n) {\n  const angleDiff = endAngle - startAngle;\n\n  // For full circles (360 degrees), draw as two semi-circles\n  if (Math.abs(angleDiff) >= 360) {\n    const start = polarToCartesian(x, y, radius, startAngle);\n    const mid = polarToCartesian(x, y, radius, startAngle + 180);\n    return [\n      \"M\",\n      start.x,\n      start.y,\n      \"A\",\n      radius,\n      radius,\n      0,\n      0,\n      1,\n      mid.x,\n      mid.y,\n      \"A\",\n      radius,\n      radius,\n      0,\n      0,\n      1,\n      start.x,\n      start.y,\n    ].join(\" \");\n  }\n\n  const start = polarToCartesian(x, y, radius, startAngle);\n  const end = polarToCartesian(x, y, radius, endAngle);\n  const largeArcFlag = angleDiff <= 180 ? \"0\" : \"1\";\n\n  return [\n    \"M\",\n    start.x,\n    start.y,\n    \"A\",\n    radius,\n    radius,\n    0,\n    largeArcFlag,\n    1,\n    end.x,\n    end.y,\n  ].join(\" \");\n}\n\ninterface GaugeContextValue {\n  value: number | null;\n  valueText: string | undefined;\n  max: number;\n  min: number;\n  state: GaugeState;\n  radius: number;\n  thickness: number;\n  size: number;\n  center: number;\n  percentage: number | null;\n  startAngle: number;\n  endAngle: number;\n  arcLength: number;\n  arcCenterY: number;\n  valueTextId?: string;\n  labelId?: string;\n}\n\nconst GaugeContext = React.createContext<GaugeContextValue | null>(null);\n\nfunction useGaugeContext(consumerName: string) {\n  const context = React.useContext(GaugeContext);\n  if (!context) {\n    throw new Error(\n      `\\`${consumerName}\\` must be used within \\`${GAUGE_NAME}\\``,\n    );\n  }\n  return context;\n}\n\ninterface GaugeProps extends DivProps {\n  value?: number | null | undefined;\n  getValueText?(value: number, min: number, max: number): string;\n  min?: number;\n  max?: number;\n  size?: number;\n  thickness?: number;\n  startAngle?: number;\n  endAngle?: number;\n}\n\nfunction Gauge(props: GaugeProps) {\n  const {\n    value: valueProp = null,\n    getValueText = getDefaultValueText,\n    min: minProp = 0,\n    max: maxProp,\n    size = 120,\n    thickness = 8,\n    startAngle = DEFAULT_START_ANGLE,\n    endAngle = DEFAULT_END_ANGLE,\n    asChild,\n    className,\n    ...rootProps\n  } = props;\n\n  if ((maxProp || maxProp === 0) && !getIsValidMaxNumber(maxProp)) {\n    if (process.env.NODE_ENV !== \"production\") {\n      console.error(getInvalidMaxError(`${maxProp}`, GAUGE_NAME));\n    }\n  }\n\n  const rawMax = getIsValidMaxNumber(maxProp) ? maxProp : DEFAULT_MAX;\n  const min = getIsValidNumber(minProp) ? minProp : 0;\n  const max = rawMax <= min ? min + 1 : rawMax;\n\n  if (process.env.NODE_ENV !== \"production\" && thickness >= size) {\n    console.warn(\n      `Gauge: thickness (${thickness}) should be less than size (${size}) for proper rendering.`,\n    );\n  }\n\n  if (valueProp !== null && !getIsValidValueNumber(valueProp, min, max)) {\n    if (process.env.NODE_ENV !== \"production\") {\n      console.error(getInvalidValueError(`${valueProp}`, GAUGE_NAME));\n    }\n  }\n\n  const value = getIsValidValueNumber(valueProp, min, max)\n    ? valueProp\n    : getIsValidNumber(valueProp) && valueProp > max\n      ? max\n      : getIsValidNumber(valueProp) && valueProp < min\n        ? min\n        : null;\n\n  const valueText = getIsValidNumber(value)\n    ? getValueText(value, min, max)\n    : undefined;\n  const state = getGaugeState(value, max);\n  const radius = Math.max(0, (size - thickness) / 2);\n  const center = size / 2;\n\n  const angleDiff = Math.abs(endAngle - startAngle);\n  const arcLength = (Math.min(angleDiff, 360) / 360) * (2 * Math.PI * radius);\n\n  const percentage = getIsValidNumber(value)\n    ? max === min\n      ? 1\n      : (value - min) / (max - min)\n    : null;\n\n  // Calculate the visual center Y of the arc for text positioning\n  // For full circles, use geometric center. For partial arcs, calculate based on bounding box\n  const angleDiffDeg = Math.abs(endAngle - startAngle);\n  const isFullCircle = angleDiffDeg >= 360;\n\n  let arcCenterY = center;\n  if (!isFullCircle) {\n    const startRad = (startAngle * Math.PI) / 180;\n    const endRad = (endAngle * Math.PI) / 180;\n\n    const startY = center - radius * Math.cos(startRad);\n    const endY = center - radius * Math.cos(endRad);\n\n    let minY = Math.min(startY, endY);\n    let maxY = Math.max(startY, endY);\n\n    const normStart = getNormalizedAngle(startAngle);\n    const normEnd = getNormalizedAngle(endAngle);\n\n    const includesTop =\n      normStart > normEnd\n        ? normStart <= 270 || normEnd >= 270\n        : normStart <= 270 && normEnd >= 270;\n    const includesBottom =\n      normStart > normEnd\n        ? normStart <= 90 || normEnd >= 90\n        : normStart <= 90 && normEnd >= 90;\n\n    if (includesTop) minY = Math.min(minY, center - radius);\n    if (includesBottom) maxY = Math.max(maxY, center + radius);\n\n    arcCenterY = (minY + maxY) / 2;\n  }\n\n  const labelId = React.useId();\n  const valueTextId = React.useId();\n\n  const contextValue = React.useMemo<GaugeContextValue>(\n    () => ({\n      value,\n      valueText,\n      max,\n      min,\n      state,\n      radius,\n      thickness,\n      size,\n      center,\n      percentage,\n      startAngle,\n      endAngle,\n      arcLength,\n      arcCenterY,\n      valueTextId,\n      labelId,\n    }),\n    [\n      value,\n      valueText,\n      max,\n      min,\n      state,\n      radius,\n      thickness,\n      size,\n      center,\n      percentage,\n      startAngle,\n      endAngle,\n      arcLength,\n      arcCenterY,\n      valueTextId,\n      labelId,\n    ],\n  );\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <GaugeContext.Provider value={contextValue}>\n      <RootPrimitive\n        role=\"meter\"\n        aria-describedby={valueText ? valueTextId : undefined}\n        aria-labelledby={labelId}\n        aria-valuemax={max}\n        aria-valuemin={min}\n        aria-valuenow={getIsValidNumber(value) ? value : undefined}\n        aria-valuetext={valueText}\n        data-state={state}\n        data-value={value ?? undefined}\n        data-max={max}\n        data-min={min}\n        data-percentage={percentage}\n        {...rootProps}\n        className={cn(\n          \"relative inline-flex w-fit flex-col items-center justify-center\",\n          className,\n        )}\n      />\n    </GaugeContext.Provider>\n  );\n}\n\nfunction GaugeIndicator(props: React.ComponentProps<\"svg\">) {\n  const { className, ...indicatorProps } = props;\n\n  const { size, state, value, max, min, percentage } =\n    useGaugeContext(INDICATOR_NAME);\n\n  return (\n    <svg\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      viewBox={`0 0 ${size} ${size}`}\n      data-state={state}\n      data-value={value ?? undefined}\n      data-max={max}\n      data-min={min}\n      data-percentage={percentage}\n      width={size}\n      height={size}\n      {...indicatorProps}\n      className={cn(\"transform\", className)}\n    />\n  );\n}\n\nfunction GaugeTrack(props: PathProps) {\n  const { className, ...trackProps } = props;\n\n  const { center, radius, startAngle, endAngle, thickness, state } =\n    useGaugeContext(TRACK_NAME);\n\n  const pathData = describeArc(center, center, radius, startAngle, endAngle);\n\n  return (\n    <path\n      data-state={state}\n      d={pathData}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={thickness}\n      strokeLinecap=\"round\"\n      vectorEffect=\"non-scaling-stroke\"\n      {...trackProps}\n      className={cn(\"text-muted-foreground/20\", className)}\n    />\n  );\n}\n\nfunction GaugeRange(props: PathProps) {\n  const { className, ...rangeProps } = props;\n\n  const {\n    center,\n    radius,\n    startAngle,\n    endAngle,\n    value,\n    max,\n    min,\n    state,\n    thickness,\n    arcLength,\n    percentage,\n  } = useGaugeContext(RANGE_NAME);\n\n  const pathData = describeArc(center, center, radius, startAngle, endAngle);\n\n  const strokeDasharray = arcLength;\n  const strokeDashoffset =\n    state === \"indeterminate\"\n      ? 0\n      : percentage !== null\n        ? arcLength - percentage * arcLength\n        : arcLength;\n\n  return (\n    <path\n      data-state={state}\n      data-value={value ?? undefined}\n      data-max={max}\n      data-min={min}\n      d={pathData}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={thickness}\n      strokeLinecap=\"round\"\n      strokeDasharray={strokeDasharray}\n      strokeDashoffset={strokeDashoffset}\n      vectorEffect=\"non-scaling-stroke\"\n      {...rangeProps}\n      className={cn(\n        \"text-primary transition-[stroke-dashoffset] duration-700 ease-out\",\n        className,\n      )}\n    />\n  );\n}\n\nfunction GaugeValueText(props: DivProps) {\n  const { asChild, className, children, style, ...valueTextProps } = props;\n\n  const { valueTextId, state, arcCenterY, valueText } =\n    useGaugeContext(VALUE_TEXT_NAME);\n\n  const ValueTextPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ValueTextPrimitive\n      id={valueTextId}\n      data-state={state}\n      {...valueTextProps}\n      style={{\n        top: `${arcCenterY}px`,\n        ...style,\n      }}\n      className={cn(\n        \"absolute right-0 left-0 flex -translate-y-1/2 items-center justify-center text-2xl font-semibold\",\n        className,\n      )}\n    >\n      {children ?? valueText}\n    </ValueTextPrimitive>\n  );\n}\n\nfunction GaugeLabel(props: DivProps) {\n  const { asChild, className, ...labelProps } = props;\n\n  const { labelId, state } = useGaugeContext(LABEL_NAME);\n\n  const LabelPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <LabelPrimitive\n      id={labelId}\n      data-state={state}\n      {...labelProps}\n      className={cn(\n        \"mt-2 text-sm font-medium text-muted-foreground\",\n        className,\n      )}\n    />\n  );\n}\n\nfunction GaugeCombined(props: GaugeProps) {\n  return (\n    <Gauge {...props}>\n      <GaugeIndicator>\n        <GaugeTrack />\n        <GaugeRange />\n      </GaugeIndicator>\n      <GaugeValueText />\n    </Gauge>\n  );\n}\n\nexport {\n  Gauge,\n  GaugeCombined,\n  GaugeIndicator,\n  GaugeLabel,\n  type GaugeProps,\n  GaugeRange,\n  GaugeTrack,\n  GaugeValueText,\n};\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}