{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "knob-slider",
  "type": "registry:component",
  "title": "Knob Slider",
  "description": "A premium radial dial component with tactile feedback, blur animations, and full theme integration.",
  "dependencies": [
    "motion",
    "react-use-measure"
  ],
  "files": [
    {
      "path": "components/watermelon/knob-slider.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport React, {\n    useState,\n    useRef,\n    useEffect,\n    useCallback,\n    useMemo,\n    useId,\n} from \"react\";\nimport {\n    motion,\n    MotionValue,\n    useSpring,\n    useTransform,\n    motionValue,\n} from \"motion/react\";\nimport useMeasure from \"react-use-measure\";\n\n/* ───────── Sliding Number ───────── */\n\nconst DIGIT_SPRING = {\n    type: \"spring\" as const,\n    stiffness: 280,\n    damping: 24,\n    mass: 0.3,\n};\n\nfunction Digit({ value, place }: { value: number; place: number }) {\n    const digit = Math.floor(value / place) % 10;\n    const mv = useMemo(() => motionValue(digit), [digit]);\n    const spring = useSpring(mv, DIGIT_SPRING);\n\n    useEffect(() => {\n        spring.set(digit);\n    }, [digit, spring]);\n\n    return (\n        <div\n            className=\"relative inline-block overflow-hidden tabular-nums\"\n            style={{ width: \"0.6em\" }}\n        >\n            <div className=\"invisible\">0</div>\n            {Array.from({ length: 10 }, (_, i) => (\n                <SlotDigit key={i} mv={spring} number={i} />\n            ))}\n        </div>\n    );\n}\n\nfunction SlotDigit({\n    mv,\n    number,\n}: {\n    mv: MotionValue<number>;\n    number: number;\n}) {\n    const id = useId();\n    const [ref, bounds] = useMeasure();\n\n    const y = useTransform(mv, (latest) => {\n        if (!bounds.height) return 0;\n        const offset = (10 + number - (latest % 10)) % 10;\n        let pos = offset * bounds.height;\n        if (offset > 5) pos -= 10 * bounds.height;\n        return pos;\n    });\n\n    if (!bounds.height)\n        return (\n            <span ref={ref} className=\"invisible absolute\">\n                {number}\n            </span>\n        );\n\n    return (\n        <motion.span\n            ref={ref}\n            style={{ y }}\n            layoutId={`${id}-${number}`}\n            transition={DIGIT_SPRING}\n            className=\"absolute inset-0 flex items-center justify-center\"\n        >\n            {number}\n        </motion.span>\n    );\n}\n\nfunction SlidingNumber({\n    value,\n    blur,\n}: {\n    value: number;\n    blur: number;\n}) {\n    const str = Math.abs(value).toString();\n    const int = parseInt(str, 10);\n    const places = str\n        .split(\"\")\n        .map((_, i) => Math.pow(10, str.length - i - 1));\n\n    return (\n        <motion.div\n            animate={{ filter: `blur(${blur}px)` }}\n            transition={{ duration: 0.15 }}\n            className=\"flex items-center justify-center font-bold tabular-nums tracking-tight\"\n            style={{\n                fontFamily: \"ui-rounded, SF Pro Rounded, system-ui, sans-serif\",\n            }}\n        >\n            {places.map((place, i) => (\n                <Digit key={`${place}-${i}`} value={int} place={place} />\n            ))}\n        </motion.div>\n    );\n}\n\n/* ───────── Knob ───────── */\n\ninterface KnobSliderProps {\n    value: number;\n    onChange: (value: number) => void;\n    min?: number;\n    max?: number;\n    size?: number;\n}\n\nexport const KnobSlider: React.FC<KnobSliderProps> = ({\n    value,\n    onChange,\n    min = 0,\n    max = 100,\n    size = 320,\n}) => {\n    const knobRef = useRef<HTMLDivElement>(null);\n    const draggingRef = useRef(false);\n    const [dragging, setDragging] = useState(false);\n\n    const tickCount = 72;\n    const innerSize = size * 0.68;\n\n    /* Blur intensity */\n    const [prev, setPrev] = useState(value);\n    const [blur, setBlur] = useState(0);\n    if (prev !== value) {\n        setBlur(dragging ? Math.min(10, Math.abs(value - prev)) : 0);\n        setPrev(value);\n    }\n\n    /* Convert pointer → snapped value */\n    const updateFromPointer = useCallback(\n        (x: number, y: number) => {\n            if (!knobRef.current) return;\n\n            const rect = knobRef.current.getBoundingClientRect();\n            const cx = rect.left + rect.width / 2;\n            const cy = rect.top + rect.height / 2;\n\n            let angle = (Math.atan2(y - cy, x - cx) * 180) / Math.PI + 90;\n            if (angle < 0) angle += 360;\n\n            /* Snap to nearest tick */\n            const tickAngle = 360 / tickCount;\n            const snappedAngle = Math.round(angle / tickAngle) * tickAngle;\n\n            const percent = snappedAngle / 360;\n            const newValue = Math.round(percent * (max - min) + min);\n\n            onChange(newValue);\n        },\n        [min, max, onChange]\n    );\n\n    useEffect(() => {\n        const move = (e: MouseEvent) =>\n            draggingRef.current && updateFromPointer(e.clientX, e.clientY);\n        const up = () => {\n            draggingRef.current = false;\n            setDragging(false);\n            setBlur(0);\n        };\n\n        if (dragging) {\n            window.addEventListener(\"mousemove\", move);\n            window.addEventListener(\"mouseup\", up);\n        }\n\n        return () => {\n            window.removeEventListener(\"mousemove\", move);\n            window.removeEventListener(\"mouseup\", up);\n        };\n    }, [dragging, updateFromPointer]);\n\n    const currentAngle = ((value - min) / (max - min)) * 360;\n\n    return (\n        <div\n            ref={knobRef}\n            onMouseDown={(e) => {\n                draggingRef.current = true;\n                setDragging(true);\n                updateFromPointer(e.clientX, e.clientY);\n            }}\n            className=\"relative flex items-center justify-center rounded-full\n                 bg-neutral-100 dark:bg-neutral-900\n                 cursor-pointer select-none\n                 transition-colors duration-300\"\n            style={{ width: size, height: size }}\n        >\n            {/* Tick Ring */}\n            <svg\n                viewBox=\"0 0 100 100\"\n                className=\"absolute inset-0 w-full h-full pointer-events-none\n                   text-neutral-400 dark:text-neutral-600\"\n            >\n                {Array.from({ length: tickCount }).map((_, i) => {\n                    const angle = (i * 360) / tickCount;\n                    return (\n                        <line\n                            key={i}\n                            x1=\"50\"\n                            y1=\"4\"\n                            x2=\"50\"\n                            y2=\"9\"\n                            transform={`rotate(${angle} 50 50)`}\n                            stroke=\"currentColor\"\n                            strokeWidth=\"0.7\"\n                            strokeLinecap=\"round\"\n                            opacity=\"0.7\"\n                        />\n                    );\n                })}\n            </svg>\n\n            {/* Arrow */}\n            <div\n                className=\"absolute inset-0 pointer-events-none\"\n                style={{ transform: `rotate(${currentAngle}deg)` }}\n            >\n                <div\n                    className=\"absolute left-1/2 -translate-x-1/2\n                     border-l-transparent border-r-transparent\n                     border-b-neutral-500 dark:border-b-neutral-300\"\n                    style={{\n                        top: size * 0.12,\n                        borderLeftWidth: size * 0.025,\n                        borderRightWidth: size * 0.025,\n                        borderBottomWidth: size * 0.045,\n                        borderStyle: \"solid\",\n                    }}\n                />\n            </div>\n\n            {/* Inner Knob */}\n            <div\n                className=\"relative rounded-full flex items-center justify-center\n                   bg-white dark:bg-neutral-800\n                   shadow-lg dark:shadow-black/40\n                   transition-colors duration-300\"\n                style={{\n                    width: innerSize,\n                    height: innerSize,\n                }}\n            >\n                <div className=\"absolute inset-0 rounded-full border border-neutral-200 dark:border-neutral-700\" />\n\n                <div\n                    className=\"text-neutral-600 dark:text-neutral-300\"\n                    style={{ fontSize: innerSize * 0.28 }}\n                >\n                    <SlidingNumber value={value} blur={blur} />\n                </div>\n            </div>\n        </div>\n    );\n};"
    }
  ]
}
