'use client'; import * as React from 'react'; import { cn } from '@/lib/utils/cn'; export interface CircularKnobProps { value: number; // -1.0 to 1.0 for pan onChange: (value: number) => void; min?: number; max?: number; step?: number; size?: number; className?: string; label?: string; formatValue?: (value: number) => string; } export function CircularKnob({ value, onChange, min = -1, max = 1, step = 0.01, size = 48, className, label, formatValue, }: CircularKnobProps) { const knobRef = React.useRef(null); const [isDragging, setIsDragging] = React.useState(false); const dragStartRef = React.useRef({ x: 0, y: 0, value: 0 }); const updateValue = React.useCallback( (clientX: number, clientY: number) => { if (!knobRef.current) return; const rect = knobRef.current.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; // Calculate vertical drag distance from start const deltaY = dragStartRef.current.y - clientY; const sensitivity = 200; // pixels for full range const range = max - min; const delta = (deltaY / sensitivity) * range; let newValue = dragStartRef.current.value + delta; // Snap to step if (step) { newValue = Math.round(newValue / step) * step; } // Clamp to range newValue = Math.max(min, Math.min(max, newValue)); onChange(newValue); }, [min, max, step, onChange] ); const handleMouseDown = React.useCallback( (e: React.MouseEvent) => { e.preventDefault(); setIsDragging(true); dragStartRef.current = { x: e.clientX, y: e.clientY, value, }; }, [value] ); const handleMouseMove = React.useCallback( (e: MouseEvent) => { if (isDragging) { updateValue(e.clientX, e.clientY); } }, [isDragging, updateValue] ); const handleMouseUp = React.useCallback(() => { setIsDragging(false); }, []); React.useEffect(() => { if (isDragging) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; } }, [isDragging, handleMouseMove, handleMouseUp]); // Calculate rotation angle (-135deg to 135deg, 270deg range) const percentage = (value - min) / (max - min); const angle = -135 + percentage * 270; const displayValue = formatValue ? formatValue(value) : value === 0 ? 'C' : value < 0 ? `L${Math.abs(Math.round(value * 100))}` : `R${Math.round(value * 100)}`; return (
{label && (
{label}
)}
{/* Outer ring */} {/* Background arc */} {/* Value arc */} {/* Knob body */}
{/* Indicator line */}
{/* Value Display */}
{displayValue}
); }