{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "knob",
  "title": "Knob",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/knob/Knob.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\n\nexport interface KnobProps {\n  /** Controlled value */\n  value?: number\n  /** Default value when uncontrolled */\n  defaultValue?: number\n  /** Emitted when the value changes */\n  onValueChange?: (value: number) => void\n  /** Also fired on every committed change (parity with the Vue `change` event) */\n  onChange?: (value: number) => void\n  min?: number\n  max?: number\n  step?: number\n  size?: number\n  strokeWidth?: number\n  valueColor?: string\n  rangeColor?: string\n  disabled?: boolean\n  readonly?: boolean\n  showValue?: boolean\n  className?: string\n  /** Custom renderer for the centered value text. */\n  renderValue?: (value: number) => React.ReactNode\n}\n\nconst startAngle = -Math.PI * 0.75\nconst endAngle = Math.PI * 0.75\nconst sweep = endAngle - startAngle\n\nfunction clamp(v: number, lo: number, hi: number) {\n  return Math.min(hi, Math.max(lo, v))\n}\n\nfunction arcPath(cx: number, cy: number, r: number, a1: number, a2: number) {\n  const x1 = cx + r * Math.cos(a1)\n  const y1 = cy + r * Math.sin(a1)\n  const x2 = cx + r * Math.cos(a2)\n  const y2 = cy + r * Math.sin(a2)\n  const large = a2 - a1 > Math.PI ? 1 : 0\n  return `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`\n}\n\nconst Knob = React.forwardRef<SVGSVGElement, KnobProps>(\n  (\n    {\n      value: controlledValue,\n      defaultValue = 0,\n      onValueChange,\n      onChange,\n      min = 0,\n      max = 100,\n      step = 1,\n      size = 100,\n      strokeWidth = 14,\n      valueColor = 'var(--primary)',\n      rangeColor = 'var(--muted)',\n      disabled = false,\n      readonly = false,\n      showValue = true,\n      className,\n      renderValue,\n    },\n    ref,\n  ) => {\n    const isControlled = controlledValue !== undefined\n    const [internal, setInternal] = React.useState(defaultValue)\n    const rawValue = isControlled ? controlledValue! : internal\n\n    const svgRef = React.useRef<SVGSVGElement | null>(null)\n    const setRefs = React.useCallback(\n      (node: SVGSVGElement | null) => {\n        svgRef.current = node\n        if (typeof ref === 'function') ref(node)\n        else if (ref) (ref as React.MutableRefObject<SVGSVGElement | null>).current = node\n      },\n      [ref],\n    )\n    const [isDragging, setIsDragging] = React.useState(false)\n\n    const value = clamp(rawValue, min, max)\n    const percent = max === min ? 0 : (value - min) / (max - min)\n\n    const rangePath = arcPath(50, 50, 40, startAngle, endAngle)\n    const valuePath = arcPath(50, 50, 40, startAngle, startAngle + sweep * percent)\n\n    function snap(v: number) {\n      const stepped = Math.round((v - min) / step) * step + min\n      return clamp(stepped, min, max)\n    }\n\n    function setValue(v: number) {\n      if (disabled || readonly) return\n      const next = snap(v)\n      if (next === rawValue) return\n      if (!isControlled) setInternal(next)\n      onValueChange?.(next)\n      onChange?.(next)\n    }\n\n    function angleFromEvent(e: React.PointerEvent): number {\n      const rect = svgRef.current!.getBoundingClientRect()\n      const cx = rect.left + rect.width / 2\n      const cy = rect.top + rect.height / 2\n      return Math.atan2(e.clientY - cy, e.clientX - cx)\n    }\n\n    function angleToValue(angle: number): number {\n      let a = angle - startAngle\n      if (a < 0) a += Math.PI * 2\n      if (a > sweep) {\n        return a < (Math.PI * 2 + sweep) / 2 ? max : min\n      }\n      return min + (a / sweep) * (max - min)\n    }\n\n    function onPointerDown(e: React.PointerEvent) {\n      if (disabled || readonly) return\n      ;(e.target as Element).setPointerCapture(e.pointerId)\n      setIsDragging(true)\n      setValue(angleToValue(angleFromEvent(e)))\n    }\n\n    function onPointerMove(e: React.PointerEvent) {\n      if (!isDragging) return\n      setValue(angleToValue(angleFromEvent(e)))\n    }\n\n    function onPointerUp(e: React.PointerEvent) {\n      if (!isDragging) return\n      setIsDragging(false)\n      ;(e.target as Element).releasePointerCapture(e.pointerId)\n    }\n\n    function onKeyDown(e: React.KeyboardEvent) {\n      if (disabled || readonly) return\n      const big = step * 10\n      switch (e.key) {\n        case 'ArrowUp':\n        case 'ArrowRight':\n          e.preventDefault()\n          setValue(value + step)\n          break\n        case 'ArrowDown':\n        case 'ArrowLeft':\n          e.preventDefault()\n          setValue(value - step)\n          break\n        case 'PageUp':\n          e.preventDefault()\n          setValue(value + big)\n          break\n        case 'PageDown':\n          e.preventDefault()\n          setValue(value - big)\n          break\n        case 'Home':\n          e.preventDefault()\n          setValue(min)\n          break\n        case 'End':\n          e.preventDefault()\n          setValue(max)\n          break\n      }\n    }\n\n    function onWheel(e: React.WheelEvent) {\n      if (disabled || readonly) return\n      // Note: React's synthetic wheel listener is passive, so preventDefault is\n      // a no-op here. A native non-passive listener is attached below to stop\n      // the page from scrolling while the knob has focus / is hovered.\n      setValue(value + (e.deltaY < 0 ? step : -step))\n    }\n\n    // Attach a non-passive wheel listener so e.preventDefault() actually blocks\n    // page scroll — React's onWheel is passive and can't cancel the scroll.\n    React.useEffect(() => {\n      const node = svgRef.current\n      if (!node) return\n      const handler = (e: WheelEvent) => {\n        if (disabled || readonly) return\n        e.preventDefault()\n      }\n      node.addEventListener('wheel', handler, { passive: false })\n      return () => node.removeEventListener('wheel', handler)\n    }, [disabled, readonly])\n\n    return (\n      <svg\n        ref={setRefs}\n        data-uipkge=\"\"\n        data-slot=\"knob\"\n        className={cn(\n          'focus-visible:ring-ring inline-block touch-none rounded-full outline-none select-none focus-visible:ring-2',\n          disabled && 'cursor-not-allowed opacity-50',\n          !disabled && !readonly && 'cursor-pointer',\n          className,\n        )}\n        width={size}\n        height={size}\n        viewBox=\"0 0 100 100\"\n        role=\"slider\"\n        aria-valuemin={min}\n        aria-valuemax={max}\n        aria-valuenow={value}\n        aria-disabled={disabled || undefined}\n        aria-readonly={readonly || undefined}\n        tabIndex={disabled ? -1 : 0}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={onPointerUp}\n        onPointerCancel={onPointerUp}\n        onKeyDown={onKeyDown}\n        onWheel={onWheel}\n      >\n        <path d={rangePath} stroke={rangeColor} strokeWidth={strokeWidth} fill=\"none\" strokeLinecap=\"round\" />\n        <path d={valuePath} stroke={valueColor} strokeWidth={strokeWidth} fill=\"none\" strokeLinecap=\"round\" />\n        {showValue && (\n          <text x=\"50\" y=\"55\" textAnchor=\"middle\" className=\"fill-foreground text-[18px] font-medium\">\n            {renderValue ? renderValue(value) : value}\n          </text>\n        )}\n      </svg>\n    )\n  },\n)\nKnob.displayName = 'Knob'\n\nexport { Knob }\n",
      "type": "registry:ui",
      "target": "~/components/ui/knob/Knob.tsx"
    },
    {
      "path": "packages/registry-react/components/knob/index.ts",
      "content": "export { Knob, type KnobProps } from './Knob'\n",
      "type": "registry:ui",
      "target": "~/components/ui/knob/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Circular dial input. Drag to rotate, keyboard accessible, SVG-rendered. Useful for compact numeric controls (volume, brightness, gauges).",
  "categories": [
    "form"
  ]
}