{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gauge-chart",
  "title": "Gauge Chart",
  "description": "Speedometer-style gauge with color zones and animated needle - custom SVG implementation",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@boldkit/utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/gauge-chart.tsx",
      "content": "/* eslint-disable react-refresh/only-export-components */\nimport * as React from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\n\nconst gaugeChartVariants = cva(\n  'relative flex items-center justify-center',\n  {\n    variants: {\n      size: {\n        sm: '',\n        md: '',\n        lg: '',\n      },\n      variant: {\n        semicircle: '',\n        full: '',\n        meter: '',\n      },\n    },\n    defaultVariants: {\n      size: 'md',\n      variant: 'semicircle',\n    },\n  }\n)\n\nexport interface GaugeChartZone {\n  from: number\n  to: number\n  color: string\n  label?: string\n}\n\nexport interface GaugeChartProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color'>,\n    VariantProps<typeof gaugeChartVariants> {\n  value: number\n  min?: number\n  max?: number\n  zones?: GaugeChartZone[]\n  label?: string\n  valueFormatter?: (value: number) => string\n  showTicks?: boolean\n  animated?: boolean\n}\n\nconst DEFAULT_ZONES: GaugeChartZone[] = [\n  { from: 0, to: 33, color: 'hsl(var(--destructive))', label: 'Low' },\n  { from: 33, to: 66, color: 'hsl(var(--warning))', label: 'Medium' },\n  { from: 66, to: 100, color: 'hsl(var(--success))', label: 'High' },\n]\n\n/**\n * Variant arc configs (angles in SVG space: 0°=east, increasing = clockwise, y points down):\n *   semicircle — 180° sweep, arc from left (180°) over the top (270°) to right (360°), open at bottom\n *   full       — 360° sweep (full ring) starting at the top\n *   meter      — same top semicircle as semicircle but with denser tick marks (every 10%)\n */\nconst VARIANT_ARC_CONFIG = {\n  semicircle: { arcStartDeg: 180, sweepDeg: 180 },\n  full:       { arcStartDeg: -90, sweepDeg: 360 }, // full 360° sweep\n  meter:      { arcStartDeg: 180, sweepDeg: 180 },\n} as const\n\ntype Variant = 'semicircle' | 'full' | 'meter'\n\nconst GaugeChart = React.forwardRef<HTMLDivElement, GaugeChartProps>(\n  (\n    {\n      value,\n      min = 0,\n      max = 100,\n      zones = DEFAULT_ZONES,\n      label,\n      valueFormatter = (v) => `${v}`,\n      showTicks = true,\n      animated = true,\n      size,\n      variant,\n      className,\n      ...props\n    },\n    ref\n  ) => {\n    const resolvedVariant: Variant = (variant as Variant) || 'semicircle'\n    const arcConfig = VARIANT_ARC_CONFIG[resolvedVariant]\n\n    const normalizedValue = Math.max(min, Math.min(max, value))\n    const percentage = max === min ? 0 : ((normalizedValue - min) / (max - min)) * 100\n\n    // SVG dimensions — full variant needs a taller canvas to show the bottom arc\n    const sizeConfig = {\n      sm: {\n        width: 140,\n        height: resolvedVariant === 'full' ? 140 : 90,\n        radius: 45,\n        strokeWidth: 10,\n        fontSize: 14,\n        labelSize: 9,\n      },\n      md: {\n        width: 180,\n        height: resolvedVariant === 'full' ? 180 : 115,\n        radius: 58,\n        strokeWidth: 12,\n        fontSize: 18,\n        labelSize: 11,\n      },\n      lg: {\n        width: 240,\n        height: resolvedVariant === 'full' ? 240 : 150,\n        radius: 76,\n        strokeWidth: 14,\n        fontSize: 22,\n        labelSize: 13,\n      },\n    }\n\n    const currentSize = size || 'md'\n    const config = sizeConfig[currentSize]\n\n    // For full variant, center is the geometric center of the SVG\n    // For semicircle/meter, center is pushed up so the arc+needle fits in the half-height canvas\n    const centerX = config.width / 2\n    const centerY =\n      resolvedVariant === 'full'\n        ? config.height / 2\n        : config.radius + config.strokeWidth + 5\n\n    const isMeter = resolvedVariant === 'meter'\n\n    const needleLength = config.radius - 8\n\n    // Convert a percentage (0–100) along the arc to an SVG angle in radians\n    const percentToAngleRad = (pct: number) => {\n      const deg = arcConfig.arcStartDeg + (pct * arcConfig.sweepDeg) / 100\n      return deg * (Math.PI / 180)\n    }\n\n    // Create an SVG arc path segment between two percentages on the gauge track\n    const createArcPath = (startPercent: number, endPercent: number, radius: number) => {\n      const startAngle = percentToAngleRad(startPercent)\n      const endAngle = percentToAngleRad(endPercent)\n\n      const startX = centerX + radius * Math.cos(startAngle)\n      const startY = centerY + radius * Math.sin(startAngle)\n\n      // A full 360° arc is degenerate in SVG (start === end point); split into two half-arcs\n      if (resolvedVariant === 'full' && Math.abs(endPercent - startPercent) >= 100) {\n        const midAngle = startAngle + Math.PI\n        const midX = centerX + radius * Math.cos(midAngle)\n        const midY = centerY + radius * Math.sin(midAngle)\n        return `M ${startX} ${startY} A ${radius} ${radius} 0 1 1 ${midX} ${midY} A ${radius} ${radius} 0 1 1 ${startX} ${startY}`\n      }\n\n      const endX = centerX + radius * Math.cos(endAngle)\n      const endY = centerY + radius * Math.sin(endAngle)\n\n      const sweepDelta = endPercent - startPercent\n      const sweepAngle = (sweepDelta / 100) * arcConfig.sweepDeg\n      const largeArcFlag = sweepAngle > 180 ? 1 : 0\n\n      return `M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`\n    }\n\n    // Needle angle: percentage along the sweep, converted to an absolute SVG rotation\n    // The needle points upward (the line is drawn rightward then rotated)\n    const needleAngle = arcConfig.arcStartDeg + (percentage * arcConfig.sweepDeg) / 100\n\n    const currentZoneColor =\n      zones.find((z) => percentage >= z.from && percentage <= z.to)?.color ||\n      'hsl(var(--primary))'\n\n    // Tick marks: semicircle/full use 5 ticks at 0/25/50/75/100 %\n    // meter variant gets denser ticks at every 10%\n    const tickPercentages =\n      resolvedVariant === 'meter'\n        ? [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]\n        : [0, 25, 50, 75, 100]\n\n    return (\n      <div\n        ref={ref}\n        className={cn(gaugeChartVariants({ size, variant }), className)}\n        style={{ maxWidth: config.width }}\n        {...props}\n      >\n        <svg\n          width=\"100%\"\n          height=\"auto\"\n          viewBox={`0 0 ${config.width} ${config.height}`}\n        >\n          {/* Background track */}\n          <path\n            d={createArcPath(0, 100, config.radius)}\n            fill=\"none\"\n            stroke=\"hsl(var(--muted))\"\n            strokeWidth={config.strokeWidth}\n            strokeLinecap=\"round\"\n          />\n\n          {/* Zone arcs */}\n          {zones.map((zone) => (\n            <path\n              key={`${zone.from}-${zone.to}-${zone.color}`}\n              d={createArcPath(zone.from, zone.to, config.radius)}\n              fill=\"none\"\n              stroke={zone.color}\n              strokeWidth={config.strokeWidth}\n              strokeLinecap=\"butt\"\n              className=\"transition duration-300\"\n            />\n          ))}\n\n          {/* Outer border */}\n          <path\n            d={createArcPath(0, 100, config.radius + config.strokeWidth / 2 + 2)}\n            fill=\"none\"\n            stroke=\"hsl(var(--foreground))\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n          />\n\n          {/* Inner border */}\n          <path\n            d={createArcPath(0, 100, config.radius - config.strokeWidth / 2 - 2)}\n            fill=\"none\"\n            stroke=\"hsl(var(--foreground))\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n          />\n\n          {/* Tick marks */}\n          {showTicks &&\n            tickPercentages.map((tick) => {\n              const angle = percentToAngleRad(tick)\n              const innerR = config.radius - config.strokeWidth / 2 - 6\n              const outerR = config.radius + config.strokeWidth / 2 + 6\n              const x1 = centerX + innerR * Math.cos(angle)\n              const y1 = centerY + innerR * Math.sin(angle)\n              const x2 = centerX + outerR * Math.cos(angle)\n              const y2 = centerY + outerR * Math.sin(angle)\n\n              return (\n                <line\n                  key={tick}\n                  x1={x1}\n                  y1={y1}\n                  x2={x2}\n                  y2={y2}\n                  stroke=\"hsl(var(--foreground))\"\n                  strokeWidth=\"2\"\n                />\n              )\n            })}\n\n          {/* Meter variant: filled progress arc using stroke-dasharray animation */}\n          {isMeter && (\n            <path\n              d={createArcPath(0, 100, config.radius)}\n              fill=\"none\"\n              stroke={currentZoneColor}\n              strokeWidth={config.strokeWidth + 4}\n              strokeLinecap=\"round\"\n              pathLength={100}\n              strokeDasharray={`${percentage} 100`}\n              style={{ transition: animated ? 'stroke-dasharray 0.5s ease-out' : 'none' }}\n            />\n          )}\n\n          {/* Needle (hidden for meter variant) */}\n          {!isMeter && (\n            <g\n              style={{\n                transform: `rotate(${needleAngle}deg)`,\n                transformOrigin: `${centerX}px ${centerY}px`,\n                transition: animated ? 'transform 0.5s ease-out' : 'none',\n                filter: 'drop-shadow(0 2px 2px rgba(0,0,0,0.3))',\n              }}\n            >\n              {/* Needle body */}\n              <line\n                x1={centerX}\n                y1={centerY}\n                x2={centerX + needleLength}\n                y2={centerY}\n                stroke=\"hsl(var(--foreground))\"\n                strokeWidth=\"3\"\n                strokeLinecap=\"round\"\n              />\n              {/* Needle tip */}\n              <circle\n                cx={centerX + needleLength}\n                cy={centerY}\n                r=\"3\"\n                fill=\"hsl(var(--primary))\"\n                stroke=\"hsl(var(--foreground))\"\n                strokeWidth=\"1.5\"\n              />\n            </g>\n          )}\n\n          {/* Center pivot (hidden for meter variant) */}\n          {!isMeter && (\n            <>\n              <circle\n                cx={centerX}\n                cy={centerY}\n                r=\"6\"\n                fill=\"hsl(var(--foreground))\"\n              />\n              <circle\n                cx={centerX}\n                cy={centerY}\n                r=\"3\"\n                fill=\"hsl(var(--background))\"\n              />\n            </>\n          )}\n\n          {/* Value display */}\n          <text\n            x={centerX}\n            y={centerY + 20}\n            textAnchor=\"middle\"\n            fill=\"hsl(var(--foreground))\"\n            fontWeight=\"900\"\n            fontSize={config.fontSize}\n            fontFamily=\"ui-monospace, monospace\"\n          >\n            {valueFormatter(normalizedValue)}\n          </text>\n\n          {/* Label */}\n          {label && (\n            <text\n              x={centerX}\n              y={centerY + 20 + config.fontSize}\n              textAnchor=\"middle\"\n              fill=\"hsl(var(--muted-foreground))\"\n              fontWeight=\"700\"\n              fontSize={config.labelSize}\n              style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}\n            >\n              {label}\n            </text>\n          )}\n\n          {/* Min/Max labels — only for semicircle and meter (full variant has no clear endpoints) */}\n          {resolvedVariant !== 'full' && (\n            <>\n              <text\n                x={centerX - config.radius - 8}\n                y={centerY + 4}\n                textAnchor=\"end\"\n                fill=\"hsl(var(--muted-foreground))\"\n                fontWeight=\"600\"\n                fontSize={config.labelSize}\n              >\n                {min}\n              </text>\n              <text\n                x={centerX + config.radius + 8}\n                y={centerY + 4}\n                textAnchor=\"start\"\n                fill=\"hsl(var(--muted-foreground))\"\n                fontWeight=\"600\"\n                fontSize={config.labelSize}\n              >\n                {max}\n              </text>\n            </>\n          )}\n        </svg>\n      </div>\n    )\n  }\n)\nGaugeChart.displayName = 'GaugeChart'\n\nexport { GaugeChart, gaugeChartVariants }\n",
      "type": "registry:ui",
      "target": "components/ui/gauge-chart.tsx"
    }
  ],
  "type": "registry:ui"
}