import { useEffect, useRef } from 'react'; import { cn } from '@/lib/utils'; // A range slider the mouse wheel can move. // // Dragging a 4-pixel-tall slider to change the power by five watts is a fussy // gesture; rolling the wheel over it is not, and it is what an operator reaches // for after using the Flex and Yaesu consoles, which have had it for a while. // // React's own onWheel is registered PASSIVE, so preventDefault() inside it is // ignored and the panel scrolls under the pointer while the value changes. The // listener therefore has to be attached natively with { passive: false }, and // the live values read through refs — the listener is installed once and would // otherwise capture the first render's value for ever. // // The Flex and Yaesu panels each grew their own copy of this before it was worth // sharing. They are left alone deliberately: they work, they are in daily use, // and their styling differs in small ways that a merge would have to guess at. export function WheelRange({ value, onChange, min = 0, max = 100, step = 1, disabled, accent = 'var(--primary)', className }: { value: number; onChange: (v: number) => void; min?: number; max?: number; step?: number; disabled?: boolean; accent?: string; className?: string; }) { const v = Math.max(min, Math.min(max, value)); const pct = max > min ? ((v - min) / (max - min)) * 100 : 0; const ref = useRef(null); const valRef = useRef(v); valRef.current = v; const cbRef = useRef(onChange); cbRef.current = onChange; const disRef = useRef(disabled); disRef.current = disabled; const stepRef = useRef(step); stepRef.current = step; const minRef = useRef(min); minRef.current = min; const maxRef = useRef(max); maxRef.current = max; useEffect(() => { const el = ref.current; if (!el) return; const onWheel = (e: WheelEvent) => { if (disRef.current) return; e.preventDefault(); const d = e.deltaY < 0 ? stepRef.current : -stepRef.current; const nv = Math.max(minRef.current, Math.min(maxRef.current, valRef.current + d)); if (nv !== valRef.current) cbRef.current(nv); }; el.addEventListener('wheel', onWheel, { passive: false }); return () => el.removeEventListener('wheel', onWheel); }, []); return ( onChange(parseInt(e.target.value, 10))} className={cn('flex-1 h-1.5 rounded-full appearance-none cursor-pointer disabled:opacity-30 disabled:cursor-default', '[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:rounded-full', '[&::-webkit-slider-thumb]:bg-card [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:cursor-pointer', className)} style={{ background: `linear-gradient(to right, ${accent} ${pct}%, color-mix(in srgb, var(--foreground) 18%, transparent) ${pct}%)`, borderColor: accent, }} /> ); }