{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gauge",
  "type": "registry:ui",
  "files": [
    {
      "path": "registry/designali/ui/gauge.tsx",
      "content": "\"use client\"\n\nimport { Slot as SlotPrimitive } from \"radix-ui\"\nimport * as React from \"react\"\nimport { cn } from \"@workspace/ui/lib/utils\"\n\nconst GAUGE_NAME = \"Gauge\"\nconst INDICATOR_NAME = \"GaugeIndicator\"\nconst TRACK_NAME = \"GaugeTrack\"\nconst RANGE_NAME = \"GaugeRange\"\nconst VALUE_TEXT_NAME = \"GaugeValueText\"\n\nconst DEFAULT_MAX = 100\nconst DEFAULT_START_ANGLE = 0\nconst DEFAULT_END_ANGLE = 360\nconst DEFAULT_SEGMENTS = 60\nconst DEFAULT_GAP = 4\nconst DEFAULT_THICKNESS = 2\n\ntype GaugeState = \"indeterminate\" | \"complete\" | \"loading\"\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean\n}\n\ninterface GaugeContextValue {\n  value: number | null\n  valueText: string | undefined\n\n  max: number\n  min: number\n\n  state: GaugeState\n\n  radius: number\n  thickness: number\n\n  size: number\n  center: number\n\n  percentage: number | null\n\n  startAngle: number\n  endAngle: number\n\n  segments: number\n  gap: number\n\n  tickInnerRadius: number\n  tickOuterRadius: number\n\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\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${GAUGE_NAME}\\``)\n  }\n\n  return context\n}\n\n/* -------------------------------------------------------------------------- */\n/* Validation                                                                 */\n/* -------------------------------------------------------------------------- */\n\nfunction getGaugeState(\n  value: number | undefined | null,\n  maxValue: number\n): GaugeState {\n  if (value == null) {\n    return \"indeterminate\"\n  }\n\n  if (value === maxValue) {\n    return \"complete\"\n  }\n\n  return \"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 >= min && value <= max\n}\n\nfunction getDefaultValueText(value: number, min: number, max: number) {\n  const percentage = max === min ? 100 : ((value - min) / (max - min)) * 100\n\n  return Math.round(percentage).toString()\n}\n\nfunction getInvalidValueError(propValue: string, componentName: string) {\n  return `Invalid prop \\`value\\` of value \\`${propValue}\\` supplied to \\`${componentName}\\`.`\n}\n\nfunction getInvalidMaxError(propValue: string, componentName: string) {\n  return `Invalid prop \\`max\\` of value \\`${propValue}\\` supplied to \\`${componentName}\\`. Only numbers greater than 0 are valid.`\n}\n\n/* -------------------------------------------------------------------------- */\n/* Geometry                                                                   */\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\n\n  return {\n    x: centerX + radius * Math.cos(angleInRadians),\n\n    y: centerY + radius * Math.sin(angleInRadians),\n  }\n}\n\n/*\n * Calculate the number of ticks that can\n * physically fit around the circle based\n * on the requested pixel gap.\n *\n * This makes `gap` an actual visual gap\n * rather than a degree value.\n */\nfunction getEffectiveSegments(\n  radius: number,\n  thickness: number,\n  requestedGap: number,\n  requestedSegments: number\n) {\n  const circumference = 2 * Math.PI * radius\n\n  const minimumSpacing = Math.max(1, thickness + requestedGap)\n\n  const maxPossibleSegments = Math.max(\n    1,\n    Math.floor(circumference / minimumSpacing)\n  )\n\n  /*\n   * Keep the requested number of segments\n   * when possible. If the requested gap\n   * cannot physically fit, reduce the\n   * number of segments.\n   */\n  return Math.min(requestedSegments, maxPossibleSegments)\n}\n\n/* -------------------------------------------------------------------------- */\n/* Gauge Props                                                                */\n/* -------------------------------------------------------------------------- */\n\ninterface GaugeProps extends DivProps {\n  value?: number | null | undefined\n\n  getValueText?(value: number, min: number, max: number): string\n\n  min?: number\n  max?: number\n\n  size?: number\n\n  /**\n   * Thickness of each tick in pixels.\n   */\n  thickness?: number\n\n  /**\n   * Gap between adjacent ticks in pixels.\n   */\n  gap?: number\n\n  startAngle?: number\n  endAngle?: number\n\n  /**\n   * Maximum / preferred number of ticks.\n   *\n   * The actual number can be reduced\n   * automatically when the requested\n   * gap needs more space.\n   */\n  segments?: number\n\n  /**\n   * Distance between the center\n   * and the inner edge of each tick.\n   */\n  tickInnerRadius?: number\n\n  /**\n   * Distance between the center\n   * and the outer edge of each tick.\n   */\n  tickOuterRadius?: number\n}\n\n/* -------------------------------------------------------------------------- */\n/* Gauge                                                                      */\n/* -------------------------------------------------------------------------- */\n\nfunction Gauge(props: GaugeProps) {\n  const {\n    value: valueProp = null,\n\n    getValueText = getDefaultValueText,\n\n    min: minProp = 0,\n    max: maxProp,\n\n    size = 200,\n\n    thickness = DEFAULT_THICKNESS,\n\n    gap = DEFAULT_GAP,\n\n    startAngle = DEFAULT_START_ANGLE,\n\n    endAngle = DEFAULT_END_ANGLE,\n\n    segments = DEFAULT_SEGMENTS,\n\n    tickInnerRadius,\n    tickOuterRadius,\n\n    asChild,\n    className,\n    ...rootProps\n  } = props\n\n  /* ------------------------------------------------------------------------ */\n  /* Max validation                                                           */\n  /* ------------------------------------------------------------------------ */\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\n  const min = getIsValidNumber(minProp) ? minProp : 0\n\n  const max = rawMax <= min ? min + 1 : rawMax\n\n  /* ------------------------------------------------------------------------ */\n  /* Safe values                                                              */\n  /* ------------------------------------------------------------------------ */\n\n  const safeThickness =\n    getIsValidNumber(thickness) && thickness > 0 ? thickness : DEFAULT_THICKNESS\n\n  const safeGap = getIsValidNumber(gap) && gap >= 0 ? gap : DEFAULT_GAP\n\n  const safeRequestedSegments =\n    Number.isInteger(segments) && segments > 0 ? segments : DEFAULT_SEGMENTS\n\n  /* ------------------------------------------------------------------------ */\n  /* Geometry                                                                 */\n  /* ------------------------------------------------------------------------ */\n\n  const center = size / 2\n\n  const radius = Math.max(0, (size - safeThickness) / 2)\n\n  const effectiveSegments = getEffectiveSegments(\n    radius,\n    safeThickness,\n    safeGap,\n    safeRequestedSegments\n  )\n\n  const percentage = getIsValidNumber(valueProp)\n    ? Math.max(0, Math.min(1, (valueProp - min) / (max - min)))\n    : null\n\n  const defaultOuterRadius = radius\n\n  const defaultInnerRadius = radius - 24\n\n  const finalOuterRadius = tickOuterRadius ?? defaultOuterRadius\n\n  const finalInnerRadius = tickInnerRadius ?? defaultInnerRadius\n\n  /* ------------------------------------------------------------------------ */\n  /* Value                                                                    */\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  if (\n    valueProp !== null &&\n    valueProp !== undefined &&\n    !getIsValidValueNumber(valueProp, min, max)\n  ) {\n    if (process.env.NODE_ENV !== \"production\") {\n      console.error(getInvalidValueError(`${valueProp}`, GAUGE_NAME))\n    }\n  }\n\n  const valueText = getIsValidNumber(value)\n    ? getValueText(value, min, max)\n    : undefined\n\n  const state = getGaugeState(value, max)\n\n  const labelId = React.useId()\n\n  const valueTextId = React.useId()\n\n  /* ------------------------------------------------------------------------ */\n  /* Context                                                                  */\n  /* ------------------------------------------------------------------------ */\n\n  const contextValue = React.useMemo(\n    () => ({\n      value,\n      valueText,\n\n      min,\n      max,\n\n      state,\n\n      radius,\n\n      thickness: safeThickness,\n\n      size,\n      center,\n\n      percentage,\n\n      startAngle,\n      endAngle,\n\n      segments: effectiveSegments,\n\n      gap: safeGap,\n\n      tickInnerRadius: finalInnerRadius,\n\n      tickOuterRadius: finalOuterRadius,\n\n      valueTextId,\n      labelId,\n    }),\n    [\n      value,\n      valueText,\n\n      min,\n      max,\n\n      state,\n\n      radius,\n      safeThickness,\n\n      size,\n      center,\n\n      percentage,\n\n      startAngle,\n      endAngle,\n\n      effectiveSegments,\n      safeGap,\n\n      finalInnerRadius,\n      finalOuterRadius,\n\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 flex inline-flex w-fit flex-col items-center justify-center\",\n          className\n        )}\n      />\n    </GaugeContext.Provider>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Indicator                                                                  */\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      width={size}\n      height={size}\n      data-state={state}\n      data-value={value ?? undefined}\n      data-max={max}\n      data-min={min}\n      data-percentage={percentage}\n      {...indicatorProps}\n      className={cn(\"block overflow-visible\", className)}\n    />\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Tick Positions                                                             */\n/* -------------------------------------------------------------------------- */\n\nfunction getTickAngles(startAngle: number, endAngle: number, segments: number) {\n  const totalAngle = endAngle - startAngle\n\n  const step = totalAngle / segments\n\n  return Array.from(\n    {\n      length: segments,\n    },\n    (_, index) => {\n      const centerAngle = startAngle + step * (index + 0.5)\n\n      return {\n        angle: centerAngle,\n      }\n    }\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Track                                                                      */\n/* -------------------------------------------------------------------------- */\n\nfunction GaugeTrack(props: React.ComponentProps<\"line\">) {\n  const { className, ...trackProps } = props\n\n  const {\n    center,\n    thickness,\n    segments,\n    startAngle,\n    endAngle,\n    tickInnerRadius,\n    tickOuterRadius,\n    state,\n  } = useGaugeContext(TRACK_NAME)\n\n  const ticks = getTickAngles(startAngle, endAngle, segments)\n\n  return (\n    <>\n      {ticks.map((tick, index) => {\n        const inner = polarToCartesian(\n          center,\n          center,\n          tickInnerRadius,\n          tick.angle\n        )\n\n        const outer = polarToCartesian(\n          center,\n          center,\n          tickOuterRadius,\n          tick.angle\n        )\n\n        return (\n          <line\n            key={index}\n            x1={inner.x}\n            y1={inner.y}\n            x2={outer.x}\n            y2={outer.y}\n\n            stroke=\"currentColor\"\n            strokeWidth={thickness}\n            strokeLinecap=\"round\"\n            vectorEffect=\"non-scaling-stroke\"\n            data-state={state}\n            {...trackProps}\n            className={cn(\"text-muted-foreground/20\", className)}\n          />\n        )\n      })}\n    </>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Range                                                                      */\n/* -------------------------------------------------------------------------- */\n\nfunction GaugeRange(props: React.ComponentProps<\"line\">) {\n  const { className, ...rangeProps } = props\n\n  const {\n    center,\n    thickness,\n    segments,\n    startAngle,\n    endAngle,\n    tickInnerRadius,\n    tickOuterRadius,\n    percentage,\n    state,\n    value,\n    max,\n    min,\n  } = useGaugeContext(RANGE_NAME)\n\n  const ticks = getTickAngles(startAngle, endAngle, segments)\n\n  return (\n    <>\n      {ticks.map((tick, index) => {\n        const tickProgress = (index + 1) / segments\n\n        const isActive =\n          state === \"complete\" ||\n          (percentage !== null && percentage >= tickProgress)\n\n        if (!isActive) {\n          return null\n        }\n\n        const inner = polarToCartesian(\n          center,\n          center,\n          tickInnerRadius,\n          tick.angle\n        )\n\n        const outer = polarToCartesian(\n          center,\n          center,\n          tickOuterRadius,\n          tick.angle\n        )\n\n        return (\n          <line\n            key={index}\n            x1={inner.x}\n            y1={inner.y}\n            x2={outer.x}\n            y2={outer.y}\n            stroke=\"currentColor\"\n            strokeWidth={thickness}\n            strokeLinecap=\"round\"\n            vectorEffect=\"non-scaling-stroke\"\n            data-state={state}\n            data-value={value ?? undefined}\n            data-max={max}\n            data-min={min}\n            data-percentage={percentage}\n            {...rangeProps}\n            className={cn(\n              \"text-primary transition-all duration-300\",\n              className\n            )}\n          />\n        )\n      })}\n    </>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Value Text                                                                 */\n/* -------------------------------------------------------------------------- */\n\nfunction GaugeValueText(props: DivProps) {\n  const { asChild, className, children, ...valueTextProps } = props\n\n  const { valueTextId, state, valueText } = 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      className={cn(\n        \"absolute inset-0 flex items-center justify-center text-3xl font-medium\",\n        className\n      )}\n    >\n      {children ?? valueText}\n    </ValueTextPrimitive>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Label                                                                      */\n/* -------------------------------------------------------------------------- */\n\nfunction GaugeLabel(props: DivProps) {\n  const { asChild, className, ...labelProps } = props\n\n  const LabelPrimitive = asChild ? SlotPrimitive.Slot : \"div\"\n\n  return (\n    <LabelPrimitive\n      {...labelProps}\n      className={cn(\"text-sm font-medium text-muted-foreground\", className)}\n    />\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Combined                                                                   */\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\n/* -------------------------------------------------------------------------- */\n/* Exports                                                                    */\n/* -------------------------------------------------------------------------- */\n\nexport {\n  Gauge,\n  GaugeCombined,\n  GaugeIndicator,\n  GaugeLabel,\n  GaugeRange,\n  GaugeTrack,\n  GaugeValueText,\n  type GaugeProps,\n}\n",
      "type": "registry:ui"
    }
  ]
}