{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "knob",
  "title": "Knob",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/knob/Knob.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = withDefaults(\n  defineProps<{\n    modelValue?: number\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    class?: HTMLAttributes['class']\n  }>(),\n  {\n    modelValue: 0,\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  },\n)\n\nconst emits = defineEmits<{\n  (e: 'update:modelValue', value: number): void\n  (e: 'change', value: number): void\n}>()\n\nconst svg = ref<SVGSVGElement | null>(null)\nconst isDragging = ref(false)\n\nconst value = computed(() => clamp(props.modelValue, props.min, props.max))\nconst percent = computed(() => {\n  if (props.max === props.min) return 0\n  return (value.value - props.min) / (props.max - props.min)\n})\n\nconst startAngle = -Math.PI * 0.75\nconst endAngle = Math.PI * 0.75\nconst sweep = endAngle - startAngle\n\nconst rangePath = computed(() => arcPath(50, 50, 40, startAngle, endAngle))\nconst valuePath = computed(() => arcPath(50, 50, 40, startAngle, startAngle + sweep * percent.value))\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\nfunction clamp(v: number, lo: number, hi: number) {\n  return Math.min(hi, Math.max(lo, v))\n}\n\nfunction snap(v: number) {\n  const stepped = Math.round((v - props.min) / props.step) * props.step + props.min\n  return clamp(stepped, props.min, props.max)\n}\n\nfunction setValue(v: number) {\n  if (props.disabled || props.readonly) return\n  const next = snap(v)\n  if (next === props.modelValue) return\n  emits('update:modelValue', next)\n  emits('change', next)\n}\n\nfunction angleFromEvent(e: PointerEvent): number {\n  const rect = svg.value!.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\nfunction 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 ? props.max : props.min\n  }\n  return props.min + (a / sweep) * (props.max - props.min)\n}\n\nfunction onPointerDown(e: PointerEvent) {\n  if (props.disabled || props.readonly) return\n  ;(e.target as Element).setPointerCapture(e.pointerId)\n  isDragging.value = true\n  setValue(angleToValue(angleFromEvent(e)))\n}\n\nfunction onPointerMove(e: PointerEvent) {\n  if (!isDragging.value) return\n  setValue(angleToValue(angleFromEvent(e)))\n}\n\nfunction onPointerUp(e: PointerEvent) {\n  if (!isDragging.value) return\n  isDragging.value = false\n  ;(e.target as Element).releasePointerCapture(e.pointerId)\n}\n\nfunction onKeyDown(e: KeyboardEvent) {\n  if (props.disabled || props.readonly) return\n  const big = props.step * 10\n  switch (e.key) {\n    case 'ArrowUp':\n    case 'ArrowRight':\n      e.preventDefault()\n      setValue(value.value + props.step)\n      break\n    case 'ArrowDown':\n    case 'ArrowLeft':\n      e.preventDefault()\n      setValue(value.value - props.step)\n      break\n    case 'PageUp':\n      e.preventDefault()\n      setValue(value.value + big)\n      break\n    case 'PageDown':\n      e.preventDefault()\n      setValue(value.value - big)\n      break\n    case 'Home':\n      e.preventDefault()\n      setValue(props.min)\n      break\n    case 'End':\n      e.preventDefault()\n      setValue(props.max)\n      break\n  }\n}\n\nfunction onWheel(e: WheelEvent) {\n  if (props.disabled || props.readonly) return\n  e.preventDefault()\n  setValue(value.value + (e.deltaY < 0 ? props.step : -props.step))\n}\n</script>\n\n<template>\n  <svg\n    ref=\"svg\"\n    data-uipkge\n    data-slot=\"knob\"\n    :class=\"\n      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        props.class,\n      )\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    @pointerdown=\"onPointerDown\"\n    @pointermove=\"onPointerMove\"\n    @pointerup=\"onPointerUp\"\n    @pointercancel=\"onPointerUp\"\n    @keydown=\"onKeyDown\"\n    @wheel=\"onWheel\"\n  >\n    <path :d=\"rangePath\" :stroke=\"rangeColor\" :stroke-width=\"strokeWidth\" fill=\"none\" stroke-linecap=\"round\" />\n    <path :d=\"valuePath\" :stroke=\"valueColor\" :stroke-width=\"strokeWidth\" fill=\"none\" stroke-linecap=\"round\" />\n    <text v-if=\"showValue\" x=\"50\" y=\"55\" text-anchor=\"middle\" class=\"fill-foreground text-[18px] font-medium\">\n      <slot name=\"value\" :value=\"value\">{{ value }}</slot>\n    </text>\n  </svg>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/knob/Knob.vue"
    },
    {
      "path": "packages/registry-vue/components/knob/index.ts",
      "content": "export { default as Knob } from './Knob.vue'\n",
      "type": "registry:ui",
      "target": "~/app/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"
  ]
}