{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "knob",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "class-variance-authority"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "knob-geometry.ts",
      "target": "components/ui/knob-geometry.ts",
      "type": "registry:ui",
      "content": "export const KNOB_SWEEP = 270;\nexport const KNOB_START_ANGLE = -135;\n\nexport const KNOB_DRAG_TRAVEL = 150;\n\nconst clamp01 = (n: number) => (n < 0 ? 0 : n > 1 ? 1 : n);\nconst clamp = (n: number, min: number, max: number) =>\n  n < min ? min : n > max ? max : n;\n\nexport function valueToTurn(value: number, min: number, max: number): number {\n  if (max === min) return 0;\n  return clamp01((value - min) / (max - min));\n}\n\nexport function turnToValue(turn: number, min: number, max: number): number {\n  return min + clamp01(turn) * (max - min);\n}\n\nexport function turnToAngle(turn: number): number {\n  return KNOB_START_ANGLE + clamp01(turn) * KNOB_SWEEP;\n}\n\nexport function arcLength(radius: number): number {\n  return (2 * Math.PI * radius * KNOB_SWEEP) / 360;\n}\n\nexport function dashForTurn(turn: number, radius: number): string {\n  const length = arcLength(radius);\n  return `${length * clamp01(turn)} ${length}`;\n}\n\nexport function arcPath(radius: number, center: number): string {\n  const at = (angle: number) => {\n    const rad = (angle * Math.PI) / 180;\n    return {\n      x: center + radius * Math.sin(rad),\n      y: center - radius * Math.cos(rad),\n    };\n  };\n  const round = (n: number) => Math.round(n * 1000) / 1000;\n  const start = at(KNOB_START_ANGLE);\n  const end = at(KNOB_START_ANGLE + KNOB_SWEEP);\n  const largeArc = KNOB_SWEEP > 180 ? 1 : 0;\n  return `M ${round(start.x)} ${round(start.y)} A ${radius} ${radius} 0 ${largeArc} 1 ${round(end.x)} ${round(end.y)}`;\n}\n\nconst decimalsOf = (step: number): number => {\n  const text = String(step);\n  if (text.includes(\"e\")) return 12;\n  const dot = text.indexOf(\".\");\n  return dot === -1 ? 0 : text.length - dot - 1;\n};\n\nexport function snapToStep(\n  value: number,\n  min: number,\n  max: number,\n  step: number,\n): number {\n  const bounded = clamp(value, min, max);\n  if (!(step > 0)) return bounded;\n  const snapped = min + Math.round((bounded - min) / step) * step;\n  return clamp(\n    Number(snapped.toFixed(Math.min(decimalsOf(step), 12))),\n    min,\n    max,\n  );\n}\n\nexport function stepValue(\n  value: number,\n  delta: number,\n  min: number,\n  max: number,\n  step: number,\n): number {\n  return snapToStep(value + delta * step, min, max, step);\n}\n\nexport function dragValue(\n  startValue: number,\n  dx: number,\n  dy: number,\n  min: number,\n  max: number,\n  step: number,\n  fine = false,\n): number {\n  const travel = dx - dy;\n  const moved = (travel / KNOB_DRAG_TRAVEL) * (max - min) * (fine ? 0.25 : 1);\n  return snapToStep(startValue + moved, min, max, step);\n}\n"
    },
    {
      "path": "use-knob-value.ts",
      "target": "components/ui/use-knob-value.ts",
      "type": "registry:ui",
      "content": "\"use client\";\nimport * as React from \"react\";\nimport { dragValue, snapToStep, stepValue } from \"./knob-geometry\";\n\nexport interface UseKnobValueOptions {\n  value?: number;\n  defaultValue?: number;\n  min: number;\n  max: number;\n  step: number;\n  disabled?: boolean;\n  onValueChange?: (value: number) => void;\n  onValueCommitted?: (value: number) => void;\n}\n\ninterface KeyIntent {\n  min: number;\n  max: number;\n  step: number;\n  shiftKey: boolean;\n}\n\nfunction nextForKey(\n  key: string,\n  value: number,\n  { min, max, step, shiftKey }: KeyIntent,\n): number | null {\n  const move = shiftKey ? step / 10 : step;\n  switch (key) {\n    case \"ArrowUp\":\n    case \"ArrowRight\":\n      return stepValue(value, 1, min, max, move);\n    case \"ArrowDown\":\n    case \"ArrowLeft\":\n      return stepValue(value, -1, min, max, move);\n    case \"PageUp\":\n      return stepValue(value, 10, min, max, move);\n    case \"PageDown\":\n      return stepValue(value, -10, min, max, move);\n    case \"Home\":\n      return min;\n    case \"End\":\n      return max;\n    default:\n      return null;\n  }\n}\n\nexport function useKnobValue({\n  value,\n  defaultValue,\n  min,\n  max,\n  step,\n  disabled,\n  onValueChange,\n  onValueCommitted,\n}: UseKnobValueOptions) {\n  const [uncontrolled, setUncontrolled] = React.useState(() =>\n    snapToStep(defaultValue ?? min, min, max, step),\n  );\n  const [dragging, setDragging] = React.useState(false);\n\n  const isControlled = value !== undefined;\n  const current = isControlled\n    ? snapToStep(value, min, max, step)\n    : uncontrolled;\n\n  const pendingRef = React.useRef(false);\n  const dragRef = React.useRef({ x: 0, y: 0, value: 0 });\n\n  const change = (next: number) => {\n    if (next === current) return;\n    if (!isControlled) setUncontrolled(next);\n    onValueChange?.(next);\n  };\n\n  const onPointerDown = (event: React.PointerEvent<HTMLElement>) => {\n    if (disabled || event.button !== 0) return;\n    event.preventDefault();\n    event.currentTarget.setPointerCapture?.(event.pointerId);\n    dragRef.current = { x: event.clientX, y: event.clientY, value: current };\n    setDragging(true);\n  };\n\n  const onPointerMove = (event: React.PointerEvent<HTMLElement>) => {\n    if (!dragging) return;\n    const start = dragRef.current;\n    change(\n      dragValue(\n        start.value,\n        event.clientX - start.x,\n        event.clientY - start.y,\n        min,\n        max,\n        step,\n        event.shiftKey,\n      ),\n    );\n  };\n\n  const endDrag = (event: React.PointerEvent<HTMLElement>) => {\n    if (!dragging) return;\n    if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {\n      event.currentTarget.releasePointerCapture(event.pointerId);\n    }\n    setDragging(false);\n    onValueCommitted?.(current);\n  };\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n    if (disabled) return;\n    const next = nextForKey(event.key, current, {\n      min,\n      max,\n      step,\n      shiftKey: event.shiftKey,\n    });\n    if (next === null) return;\n    event.preventDefault();\n    pendingRef.current = true;\n    change(next);\n  };\n\n  const onKeyUp = () => {\n    if (!pendingRef.current) return;\n    pendingRef.current = false;\n    onValueCommitted?.(current);\n  };\n\n  return {\n    value: current,\n    dragging,\n    handlers: {\n      onPointerDown,\n      onPointerMove,\n      onPointerUp: endDrag,\n      onPointerCancel: endDrag,\n      onKeyDown,\n      onKeyUp,\n    },\n  };\n}\n"
    },
    {
      "path": "knob.tsx",
      "target": "components/ui/knob.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  arcPath,\n  dashForTurn,\n  turnToAngle,\n  valueToTurn,\n} from \"./knob-geometry\";\nimport { useKnobValue } from \"./use-knob-value\";\n\nconst SIZES = {\n  sm: { box: 44, stroke: 3, tick: 7, thickness: 2 },\n  md: { box: 64, stroke: 4, tick: 10, thickness: 2.5 },\n  lg: { box: 96, stroke: 6, tick: 14, thickness: 3 },\n} as const;\n\nconst controlVariants = cva(\n  cn(\n    \"group relative touch-none select-none rounded-full outline-none\",\n    \"cursor-grab data-[dragging]:cursor-grabbing\",\n    \"focus-visible:ring-focus\",\n    \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50\",\n    \"after:absolute after:inset-0 after:content-['']\",\n  ),\n  {\n    variants: { size: { sm: \"\", md: \"\", lg: \"\" } },\n    defaultVariants: { size: \"md\" },\n  },\n);\n\nexport interface KnobProps\n  extends Omit<\n      React.HTMLAttributes<HTMLDivElement>,\n      \"defaultValue\" | \"onChange\" | \"children\"\n    >,\n    VariantProps<typeof controlVariants> {\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (value: number) => void;\n  onValueCommitted?: (value: number) => void;\n  min?: number;\n  max?: number;\n  step?: number;\n  label?: React.ReactNode;\n  showValue?: boolean;\n  formatValue?: (value: number) => string;\n  disabled?: boolean;\n}\n\nexport const Knob = React.forwardRef<HTMLDivElement, KnobProps>(\n  (\n    {\n      className,\n      size,\n      value,\n      defaultValue,\n      onValueChange,\n      onValueCommitted,\n      min = 0,\n      max = 100,\n      step = 1,\n      label,\n      showValue,\n      formatValue,\n      disabled,\n      \"aria-label\": ariaLabel,\n      \"aria-labelledby\": ariaLabelledBy,\n      ...props\n    },\n    ref,\n  ) => {\n    const labelId = React.useId();\n    const metrics = SIZES[size ?? \"md\"];\n    const {\n      value: current,\n      dragging,\n      handlers,\n    } = useKnobValue({\n      value,\n      defaultValue,\n      min,\n      max,\n      step,\n      disabled,\n      onValueChange,\n      onValueCommitted,\n    });\n\n    const center = metrics.box / 2;\n    const ringRadius = center - metrics.stroke / 2;\n    const bodyRadius = ringRadius - metrics.stroke * 1.5;\n    const anchorRadius = bodyRadius - metrics.tick;\n    const head = metrics.stroke * 1.5;\n    const thumbLength = dragging\n      ? ringRadius + metrics.stroke * 0.75 - anchorRadius\n      : metrics.tick;\n    const turn = valueToTurn(current, min, max);\n    const path = arcPath(ringRadius, center);\n    const formatted = formatValue ? formatValue(current) : String(current);\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"inline-flex flex-col items-center gap-2\", className)}\n        {...props}\n      >\n        {label ? (\n          <span\n            id={labelId}\n            className={cn(\n              \"text-sm text-ink select-none\",\n              disabled && \"opacity-50\",\n            )}\n          >\n            {label}\n          </span>\n        ) : null}\n\n        <div\n          role=\"slider\"\n          tabIndex={disabled ? -1 : 0}\n          aria-valuemin={min}\n          aria-valuemax={max}\n          aria-valuenow={current}\n          aria-valuetext={formatted}\n          aria-label={ariaLabel}\n          aria-labelledby={label ? labelId : ariaLabelledBy}\n          aria-disabled={disabled || undefined}\n          data-dragging={dragging ? \"\" : undefined}\n          data-disabled={disabled ? \"\" : undefined}\n          className={controlVariants({ size })}\n          style={{ width: metrics.box, height: metrics.box }}\n          {...(disabled ? {} : handlers)}\n        >\n          <svg\n            viewBox={`0 0 ${metrics.box} ${metrics.box}`}\n            width={metrics.box}\n            height={metrics.box}\n            aria-hidden=\"true\"\n            focusable=\"false\"\n            className=\"block\"\n          >\n            <path\n              d={path}\n              fill=\"none\"\n              strokeWidth={metrics.stroke}\n              strokeLinecap=\"round\"\n              className=\"stroke-border-strong\"\n            />\n            <path\n              d={path}\n              fill=\"none\"\n              strokeWidth={metrics.stroke}\n              strokeLinecap=\"round\"\n              strokeDasharray={dashForTurn(turn, ringRadius)}\n              className={cn(\n                \"stroke-accent\",\n                dragging\n                  ? \"transition-none\"\n                  : \"transition-[stroke-dasharray] duration-fast ease-soft motion-reduce:transition-none\",\n              )}\n            />\n          </svg>\n\n          <div\n            className=\"absolute rounded-full bg-sunken shadow-raised\"\n            style={{ inset: center - bodyRadius }}\n          />\n\n          <div\n            className={cn(\n              \"pointer-events-none absolute inset-0\",\n              dragging\n                ? \"transition-none\"\n                : \"transition-transform duration-fast ease-soft motion-reduce:transition-none\",\n            )}\n            style={{ transform: `rotate(${turnToAngle(turn)}deg)` }}\n          >\n            <div\n              className=\"absolute left-1/2\"\n              style={{ top: center - anchorRadius }}\n            >\n              <div\n                className={cn(\n                  \"absolute top-0 left-0 rounded-full bg-ink/70\",\n                  \"transition-[width,height,background-color] duration-fast ease-soft motion-reduce:transition-none\",\n                  \"group-hover:bg-ink\",\n                  \"group-data-[dragging]:bg-ink/30\",\n                )}\n                style={{\n                  width: dragging ? head : metrics.thickness,\n                  height: thumbLength,\n                  transform: \"translate(-50%, -100%)\",\n                }}\n              >\n                <span\n                  className=\"absolute top-0 left-1/2 -translate-x-1/2 rounded-full bg-accent opacity-0 transition-opacity duration-fast ease-soft group-data-[dragging]:opacity-100 group-data-[dragging]:glow-accent-strong motion-reduce:transition-none\"\n                  style={{ width: head, height: head }}\n                />\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {showValue ? (\n          <span className=\"font-mono text-xs tabular-nums text-muted\">\n            {formatted}\n          </span>\n        ) : null}\n      </div>\n    );\n  },\n);\n\nKnob.displayName = \"Knob\";\n"
    }
  ]
}