{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "threshold-ring",
  "type": "registry:component",
  "title": "Threshold Ring",
  "description": "Compact ring gauge expressing how close a value is to a threshold.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/threshold-ring/threshold-ring.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type ReactNode,\n} from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/**\n * Tone of the active arc — drives its stroke color.\n *\n * @public\n */\nexport type ThresholdRingTone = \"danger\" | \"neutral\" | \"success\" | \"warn\";\n\nconst TONE_STROKE: Record<ThresholdRingTone, string> = {\n  danger: \"stroke-red-500\",\n  neutral: \"stroke-foreground\",\n  success: \"stroke-emerald-500\",\n  warn: \"stroke-amber-500\",\n};\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type ThresholdRingLabels = {\n  /** Aria-label override. Defaults to `\"Threshold ring\"`. */\n  region?: string;\n};\n\nconst DEFAULT_LABELS = {\n  region: \"Threshold ring\",\n} as const satisfies Required<ThresholdRingLabels>;\n\n/**\n * Props for {@link ThresholdRing}.\n *\n * @public\n */\nexport type ThresholdRingProps = {\n  /** Optional center label (formatted by host — e.g. `\"82%\"`). */\n  centerLabel?: ReactNode;\n  /** Localizable strings. */\n  labels?: ThresholdRingLabels;\n  /** Upper bound of the ring's value range. Defaults to `1`. */\n  max?: number;\n  /** Outer diameter in pixels. Defaults to `64`. */\n  size?: number;\n  /** Stroke width in pixels. Defaults to `6`. */\n  stroke?: number;\n  /** Optional threshold marker `0..max` rendered as a small notch. */\n  threshold?: number;\n  /** Tone of the active arc. Defaults to `\"neutral\"`. */\n  tone?: ThresholdRingTone;\n  /** Current value `0..max`. */\n  value: number;\n} & ComponentPropsWithoutRef<\"svg\">;\n\nconst clamp = (v: number, min: number, max: number): number => {\n  if (v < min) {\n    return min;\n  }\n  if (v > max) {\n    return max;\n  }\n  return v;\n};\n\nconst polar = (input: {\n  angle: number;\n  cx: number;\n  cy: number;\n  r: number;\n}): { x: number; y: number } => {\n  const rad = (input.angle - 90) * (Math.PI / 180);\n  return {\n    x: input.cx + input.r * Math.cos(rad),\n    y: input.cy + input.r * Math.sin(rad),\n  };\n};\n\ntype Geometry = {\n  circumference: number;\n  cx: number;\n  cy: number;\n  r: number;\n  ratio: number;\n  size: number;\n  stroke: number;\n  tickAngle: null | number;\n};\n\nconst computeGeometry = (input: {\n  max: number;\n  size: number;\n  stroke: number;\n  threshold?: number;\n  value: number;\n}): Geometry => {\n  const safeMax = input.max <= 0 ? 1 : input.max;\n  const ratio = clamp(input.value / safeMax, 0, 1);\n  const cx = input.size / 2;\n  const cy = input.size / 2;\n  const r = (input.size - input.stroke) / 2;\n  const circumference = 2 * Math.PI * r;\n  const tickAngle =\n    input.threshold === undefined\n      ? null\n      : clamp(input.threshold / safeMax, 0, 1) * 360;\n  return {\n    circumference,\n    cx,\n    cy,\n    r,\n    ratio,\n    size: input.size,\n    stroke: input.stroke,\n    tickAngle,\n  };\n};\n\nconst Tick = (props: { geom: Geometry }): null | React.ReactElement => {\n  const { geom } = props;\n  if (geom.tickAngle === null) {\n    return null;\n  }\n  const inner = polar({\n    angle: geom.tickAngle,\n    cx: geom.cx,\n    cy: geom.cy,\n    r: geom.r - geom.stroke / 2,\n  });\n  const outer = polar({\n    angle: geom.tickAngle,\n    cx: geom.cx,\n    cy: geom.cy,\n    r: geom.r + geom.stroke / 2,\n  });\n  return (\n    <line\n      className=\"stroke-foreground/80\"\n      data-threshold-ring-tick\n      strokeLinecap=\"round\"\n      strokeWidth={2}\n      x1={inner.x}\n      x2={outer.x}\n      y1={inner.y}\n      y2={outer.y}\n    />\n  );\n};\n\n/**\n * Compact ring gauge expressing how close a value is to a threshold.\n * Pure presentation; the host supplies the value, threshold, and tone.\n * Use to overlay budget / quota / SLA indicators on canvas objects\n * without consuming card real-estate.\n *\n * Distinct from `MetricGauge`: this primitive is small, headless, and\n * meant to attach to a runtime object — not a dashboard tile.\n *\n * @example\n * ```tsx\n * <ThresholdRing value={0.82} threshold={0.7} tone=\"warn\" centerLabel=\"82%\" />\n * ```\n *\n * @public\n */\nexport const ThresholdRing = forwardRef<SVGSVGElement, ThresholdRingProps>(\n  (props, ref) => {\n    const {\n      centerLabel,\n      className,\n      labels,\n      max = 1,\n      size = 64,\n      stroke = 6,\n      threshold,\n      tone = \"neutral\",\n      value,\n      ...rest\n    } = props;\n    const resolvedLabels = { ...DEFAULT_LABELS, ...labels };\n    const geom = computeGeometry({ max, size, stroke, threshold, value });\n    const dash = `${geom.ratio * geom.circumference} ${geom.circumference}`;\n    return (\n      <svg\n        aria-label={resolvedLabels.region}\n        className={cn(\"inline-block\", className)}\n        data-threshold-ring\n        data-threshold-tone={tone}\n        height={geom.size}\n        ref={ref}\n        role=\"img\"\n        viewBox={`0 0 ${geom.size} ${geom.size}`}\n        width={geom.size}\n        {...rest}\n      >\n        <circle\n          className=\"stroke-muted\"\n          cx={geom.cx}\n          cy={geom.cy}\n          fill=\"none\"\n          r={geom.r}\n          strokeWidth={geom.stroke}\n        />\n        <circle\n          className={cn(\"transition-all duration-300\", TONE_STROKE[tone])}\n          cx={geom.cx}\n          cy={geom.cy}\n          data-threshold-ring-arc\n          fill=\"none\"\n          r={geom.r}\n          strokeDasharray={dash}\n          strokeLinecap=\"round\"\n          strokeWidth={geom.stroke}\n          transform={`rotate(-90 ${geom.cx} ${geom.cy})`}\n        />\n        <Tick geom={geom} />\n        {centerLabel ? (\n          <text\n            className=\"fill-foreground text-[10px] font-semibold\"\n            data-threshold-ring-label\n            dominantBaseline=\"central\"\n            textAnchor=\"middle\"\n            x={geom.cx}\n            y={geom.cy}\n          >\n            {centerLabel}\n          </text>\n        ) : null}\n      </svg>\n    );\n  },\n);\nThresholdRing.displayName = \"ThresholdRing\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
