{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gauge",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/watermelon-ui/gauge.tsx",
      "content": "import { useEffect, useState } from \"react\"\nimport type { CSSProperties, SVGProps } from \"react\"\nimport { useMotionValue, useSpring } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface GaugeProps extends Omit<SVGProps<SVGSVGElement>, \"className\"> {\n  value: number\n  size?: number | string\n  gapPercent?: number\n  strokeWidth?: number\n  equal?: boolean\n  showValue?: boolean\n  showPercentage?: boolean\n  primary?:\n    | \"danger\"\n    | \"warning\"\n    | \"success\"\n    | \"info\"\n    | string\n    | { [key: number]: string }\n  secondary?:\n    | \"danger\"\n    | \"warning\"\n    | \"success\"\n    | \"info\"\n    | string\n    | { [key: number]: string }\n  gradient?: boolean\n  multiRing?: {\n    enabled: boolean\n    rings?: Array<{\n      value: number\n      color: string\n      strokeWidth?: number\n      opacity?: number\n    }>\n  }\n  thresholds?: Array<{\n    value: number\n    color: string\n    label?: string\n  }>\n  gaugeType?: \"full\" | \"half\" | \"quarter\"\n  transition?: {\n    length?: number\n    step?: number\n    delay?: number\n  }\n  className?:\n    | string\n    | {\n        svgClassName?: string\n        primaryClassName?: string\n        secondaryClassName?: string\n        textClassName?: string\n        labelClassName?: string\n      }\n  label?: string\n  unit?: string\n  min?: number\n  max?: number\n  tickMarks?: boolean\n  glowEffect?: boolean\n}\n\nexport function Gauge({\n  value,\n  size = 150,\n  gapPercent = 5,\n  strokeWidth = 10,\n  equal = false,\n  showValue = true,\n  showPercentage = false,\n  primary,\n  secondary,\n  gradient = false,\n  multiRing,\n  thresholds,\n  gaugeType = \"full\",\n  transition = {\n    length: 1000,\n    step: 200,\n    delay: 0,\n  },\n  className,\n  label,\n  unit = \"%\",\n  min = 0,\n  max = 100,\n  tickMarks = false,\n  glowEffect = false,\n  ...props\n}: GaugeProps) {\n  const circleSize = 100\n  const radius = circleSize / 2 - strokeWidth / 2\n  const circumference = 2 * Math.PI * radius\n  const percentToDegree = 360 / 100\n\n  const offsetFactor = equal ? 0.5 : 0\n  const offsetFactorSecondary = 1 - offsetFactor\n\n  const { formattedValue: animatedValue, rawValue: animatedRawValue } =\n    useNumberCounter({\n      value,\n      delay: (transition?.delay ?? 0) / 1000,\n      decimalPlaces: value % 1 !== 0 ? 1 : 0,\n    })\n\n  const getGaugeConfig = () => {\n    switch (gaugeType) {\n      case \"half\":\n        return {\n          startAngle: -90,\n          endAngle: 90,\n          circumferenceFactor: 0.5,\n          viewBox: `0 25 ${circleSize} 50`,\n        }\n      case \"quarter\":\n        return {\n          startAngle: 0,\n          endAngle: 90,\n          circumferenceFactor: 0.25,\n          viewBox: `25 25 50 50`,\n        }\n      default:\n        return {\n          startAngle: -90,\n          endAngle: 270,\n          circumferenceFactor: 1,\n          viewBox: `0 0 ${circleSize} ${circleSize}`,\n        }\n    }\n  }\n\n  // Use the animated raw value for circle calculations instead of the static value\n  const strokePercent = animatedRawValue\n\n  const gaugeConfig = getGaugeConfig()\n  const adjustedCircumference = circumference * gaugeConfig.circumferenceFactor\n  const adjustedPercentToPx = adjustedCircumference / 100\n\n  const primaryStrokeDasharray = () => {\n    if (\n      offsetFactor > 0 &&\n      strokePercent > 100 - gapPercent * 2 * offsetFactor\n    ) {\n      const subtract = -strokePercent + 100\n      return `${Math.max(strokePercent * adjustedPercentToPx - subtract * adjustedPercentToPx, 0)} ${adjustedCircumference}`\n    } else {\n      const subtract = gapPercent * 2 * offsetFactor\n      return `${Math.max(strokePercent * adjustedPercentToPx - subtract * adjustedPercentToPx, 0)} ${adjustedCircumference}`\n    }\n  }\n\n  const secondaryStrokeDasharray = () => {\n    if (\n      offsetFactorSecondary < 1 &&\n      strokePercent < gapPercent * 2 * offsetFactorSecondary\n    ) {\n      const subtract = strokePercent\n      return `${Math.max((100 - strokePercent) * adjustedPercentToPx - subtract * adjustedPercentToPx, 0)} ${adjustedCircumference}`\n    } else {\n      const subtract = gapPercent * 2 * offsetFactorSecondary\n      return `${Math.max((100 - strokePercent) * adjustedPercentToPx - subtract * adjustedPercentToPx, 0)} ${adjustedCircumference}`\n    }\n  }\n\n  const primaryTransform = () => {\n    if (\n      offsetFactor > 0 &&\n      strokePercent > 100 - gapPercent * 2 * offsetFactor\n    ) {\n      const add = 0.5 * (-strokePercent + 100)\n      return `rotate(${-90 + add * percentToDegree}deg)`\n    } else {\n      const add = gapPercent * offsetFactor\n      return `rotate(${-90 + add * percentToDegree}deg)`\n    }\n  }\n\n  const secondaryTransform = () => {\n    if (\n      offsetFactorSecondary < 1 &&\n      strokePercent < gapPercent * 2 * offsetFactorSecondary\n    ) {\n      const subtract = 0.5 * strokePercent\n      return `rotate(${360 - 90 - subtract * percentToDegree}deg) scaleY(-1)`\n    } else {\n      const subtract = gapPercent * offsetFactorSecondary\n      return `rotate(${360 - 90 - subtract * percentToDegree}deg) scaleY(-1)`\n    }\n  }\n\n  const getColor = (colorProp: typeof primary, isSecondary = false) => {\n    const defaultColors = isSecondary\n      ? {\n          danger: \"#fecaca\",\n          warning: \"#fde68a\",\n          info: \"#bfdbfe\",\n          success: \"#bbf7d0\",\n        }\n      : {\n          danger: \"#dc2626\",\n          warning: \"#f59e0b\",\n          info: \"#3b82f6\",\n          success: \"#22c55e\",\n        }\n\n    if (!colorProp) {\n      if (isSecondary) return \"rgba(85, 85, 85, 0.2)\"\n      return strokePercent <= 25\n        ? defaultColors.danger\n        : strokePercent <= 50\n          ? defaultColors.warning\n          : strokePercent <= 75\n            ? defaultColors.info\n            : defaultColors.success\n    }\n\n    if (typeof colorProp === \"string\") {\n      return defaultColors[colorProp as keyof typeof defaultColors] || colorProp\n    }\n\n    if (typeof colorProp === \"object\") {\n      const keys = Object.keys(colorProp).sort((a, b) => Number(a) - Number(b))\n      const checkValue = isSecondary ? 100 - strokePercent : strokePercent\n\n      for (let i = 0; i < keys.length; i++) {\n        const currentKey = Number(keys[i])\n        const nextKey = Number(keys[i + 1])\n        if (checkValue >= currentKey && (checkValue < nextKey || !nextKey)) {\n          const color = colorProp[currentKey]\n          return defaultColors[color as keyof typeof defaultColors] || color\n        }\n      }\n    }\n\n    return isSecondary ? \"#e5e7eb\" : \"#3b82f6\"\n  }\n\n  const primaryStroke = getColor(primary)\n  const secondaryStroke = getColor(secondary, true)\n\n  const primaryOpacity = () => {\n    if (\n      offsetFactor > 0 &&\n      strokePercent < gapPercent * 2 * offsetFactor &&\n      strokePercent < gapPercent * 2 * offsetFactorSecondary\n    ) {\n      return 0\n    } else return 1\n  }\n\n  const secondaryOpacity = () => {\n    if (\n      (offsetFactor === 0 && strokePercent > 100 - gapPercent * 2) ||\n      (offsetFactor > 0 &&\n        strokePercent > 100 - gapPercent * 2 * offsetFactor &&\n        strokePercent > 100 - gapPercent * 2 * offsetFactorSecondary)\n    ) {\n      return 0\n    } else return 1\n  }\n\n  const circleStyles: CSSProperties = {\n    strokeLinecap: \"round\",\n    strokeLinejoin: \"round\",\n    strokeDashoffset: 0,\n    strokeWidth: strokeWidth,\n    // Remove CSS transition since we're using animated values\n    transformOrigin: \"50% 50%\",\n    shapeRendering: \"geometricPrecision\",\n  }\n\n  const glowStyles = glowEffect\n    ? {\n        filter: `\n        drop-shadow(0 0 2px ${primaryStroke}80)\n        drop-shadow(0 0 6px ${primaryStroke}60)\n        drop-shadow(0 0 12px ${primaryStroke}40)\n        drop-shadow(0 0 20px ${primaryStroke}20)\n      `,\n      }\n    : {}\n\n  const generateTickMarks = () => {\n    if (!tickMarks) return null\n    const ticks = []\n    const tickCount = 8\n\n    for (let i = 0; i <= tickCount; i++) {\n      const angle =\n        (i / tickCount) * (gaugeConfig.endAngle - gaugeConfig.startAngle) +\n        gaugeConfig.startAngle\n      const tickRadius = radius - strokeWidth / 2\n      const tickLength = 6\n\n      const x1 =\n        circleSize / 2 +\n        (tickRadius - tickLength) * Math.cos((angle * Math.PI) / 180)\n      const y1 =\n        circleSize / 2 +\n        (tickRadius - tickLength) * Math.sin((angle * Math.PI) / 180)\n      const x2 = circleSize / 2 + tickRadius * Math.cos((angle * Math.PI) / 180)\n      const y2 = circleSize / 2 + tickRadius * Math.sin((angle * Math.PI) / 180)\n\n      ticks.push(\n        <line\n          key={i}\n          x1={x1}\n          y1={y1}\n          x2={x2}\n          y2={y2}\n          stroke=\"currentColor\"\n          strokeWidth=\"1\"\n          opacity=\"0.3\"\n        />\n      )\n    }\n    return ticks\n  }\n\n  return (\n    <div className=\"relative inline-block\">\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox={`0 0 ${circleSize} ${circleSize}`}\n        shapeRendering=\"crispEdges\"\n        width={size}\n        height={size}\n        style={{ userSelect: \"none\", ...glowStyles }}\n        fill=\"none\"\n        className={cn(\n          \"\",\n          typeof className === \"string\" ? className : className?.svgClassName\n        )}\n        {...props}\n      >\n        {gradient && (\n          <defs>\n            <linearGradient\n              id=\"primaryGradient\"\n              x1=\"0%\"\n              y1=\"0%\"\n              x2=\"100%\"\n              y2=\"0%\"\n            >\n              <stop offset=\"0%\" stopColor={primaryStroke} stopOpacity=\"0.3\" />\n              <stop offset=\"100%\" stopColor={primaryStroke} stopOpacity=\"1\" />\n            </linearGradient>\n          </defs>\n        )}\n\n        {generateTickMarks()}\n\n        {multiRing?.enabled &&\n          multiRing.rings?.map((ring, index) => (\n            <circle\n              key={`ring-${index}`}\n              cx={circleSize / 2}\n              cy={circleSize / 2}\n              r={radius - (index + 1) * (strokeWidth + 2)}\n              style={{\n                ...circleStyles,\n                strokeWidth: ring.strokeWidth || strokeWidth - 2,\n                strokeDasharray: `${(ring.value / 100) * adjustedCircumference} ${adjustedCircumference}`,\n                transform: primaryTransform(),\n                stroke: ring.color,\n                opacity: ring.opacity,\n              }}\n            />\n          ))}\n\n        <circle\n          cx={circleSize / 2}\n          cy={circleSize / 2}\n          r={radius}\n          style={{\n            ...circleStyles,\n            strokeDasharray: secondaryStrokeDasharray(),\n            transform: secondaryTransform(),\n            stroke: secondaryStroke,\n            opacity: secondaryOpacity(),\n          }}\n          className={cn(\n            \"\",\n            typeof className === \"object\" && className?.secondaryClassName\n          )}\n        />\n\n        <circle\n          cx={circleSize / 2}\n          cy={circleSize / 2}\n          r={radius}\n          style={{\n            ...circleStyles,\n            strokeDasharray: primaryStrokeDasharray(),\n            transform: primaryTransform(),\n            stroke: gradient ? \"url(#primaryGradient)\" : primaryStroke,\n            opacity: primaryOpacity(),\n          }}\n          className={cn(\n            \"\",\n            typeof className === \"object\" && className?.primaryClassName\n          )}\n        />\n        {thresholds?.map((threshold, index) => {\n          const thresholdPercent = ((threshold.value - min) / (max - min)) * 100\n          const angle =\n            (thresholdPercent / 100) *\n              (gaugeConfig.endAngle - gaugeConfig.startAngle) +\n            gaugeConfig.startAngle\n          const indicatorRadius = radius + strokeWidth / 2 + 5\n          const x =\n            circleSize / 2 + indicatorRadius * Math.cos((angle * Math.PI) / 180)\n          const y =\n            circleSize / 2 + indicatorRadius * Math.sin((angle * Math.PI) / 180)\n\n          return (\n            <circle\n              key={`threshold-${index}`}\n              cx={x}\n              cy={y}\n              r=\"2\"\n              fill={threshold.color}\n            />\n          )\n        })}\n\n        {showValue && (\n          <g>\n            <text\n              x={circleSize / 2}\n              y={circleSize / 2}\n              textAnchor=\"middle\"\n              dominantBaseline=\"middle\"\n              alignmentBaseline=\"central\"\n              fill=\"currentColor\"\n              fontSize={30}\n              fontWeight=\"700\"\n              className={cn(\n                \"font-bold\",\n                typeof className === \"object\" && className?.textClassName\n              )}\n              style={{ userSelect: \"none\" }}\n            >\n              {animatedValue}\n              {showPercentage && unit}\n            </text>\n          </g>\n        )}\n        {label && (\n          <text\n            x={circleSize / 2}\n            y={circleSize / 2 + 20}\n            textAnchor=\"middle\"\n            dominantBaseline=\"middle\"\n            fontSize={8}\n            fontWeight=\"400\"\n            className=\"fill-muted-foreground\"\n            style={{ userSelect: \"none\" }}\n          >\n            {label}\n          </text>\n        )}\n      </svg>\n    </div>\n  )\n}\n\n// Hook version for use in SVG contexts - now returns both formatted text and raw animated value\nexport function useNumberCounter({\n  value,\n  direction = \"up\",\n  delay = 0,\n  decimalPlaces = 0,\n}: {\n  value: number\n  direction?: \"up\" | \"down\"\n  delay?: number\n  decimalPlaces?: number\n}) {\n  const [displayValue, setDisplayValue] = useState(\n    direction === \"down\" ? value : 0\n  )\n  const [rawValue, setRawValue] = useState(direction === \"down\" ? value : 0)\n  const [isInView, setIsInView] = useState(false)\n\n  const motionValue = useMotionValue(direction === \"down\" ? value : 0)\n  const springValue = useSpring(motionValue, {\n    damping: 60,\n    stiffness: 100,\n  })\n\n  // Set initial display value\n  useEffect(() => {\n    const initialValue = direction === \"down\" ? value : 0\n    setDisplayValue(initialValue)\n    setRawValue(initialValue)\n  }, [direction, value])\n\n  // Simulate useInView for SVG context\n  useEffect(() => {\n    const timer = setTimeout(() => setIsInView(true), 100)\n    return () => clearTimeout(timer)\n  }, [])\n\n  // Trigger animation after delay\n  useEffect(() => {\n    if (isInView) {\n      const timeout = setTimeout(() => {\n        motionValue.set(direction === \"down\" ? 0 : value)\n      }, delay * 1000)\n      return () => clearTimeout(timeout)\n    }\n  }, [motionValue, isInView, delay, value, direction])\n\n  // Update display value when spring value changes\n  useEffect(() => {\n    const unsubscribe = springValue.on(\"change\", (latest) => {\n      const formattedValue = Number(latest.toFixed(decimalPlaces))\n      setDisplayValue(formattedValue)\n      setRawValue(latest) // Keep the raw animated value for circle animation\n    })\n    return unsubscribe\n  }, [springValue, decimalPlaces])\n\n  const formattedDisplayValue = Intl.NumberFormat(\"en-US\", {\n    minimumFractionDigits: decimalPlaces,\n    maximumFractionDigits: decimalPlaces,\n  }).format(displayValue)\n\n  return {\n    formattedValue: formattedDisplayValue,\n    rawValue: rawValue,\n  }\n}\n",
      "type": "registry:ui",
      "target": "components/ui/gauge.tsx"
    }
  ],
  "type": "registry:ui"
}
