// Camera Tilt Control // Purpose: Defines the Camera Tilt Control module and the local helpers/components used in this file. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. import { useEffect, useRef, useState } from 'react'; function formatDegrees(value) { if (typeof value !== 'number' || Number.isNaN(value)) return '—'; return `${value > 0 ? '+' : ''}${value.toFixed(1)}°`; } function KeyPill({ label }) { if (!label) return null; return {label}; } export default function CameraTiltControl({ value, min, max, step = 0.5, label = 'Camera Tilt', orientation = 'horizontal', onChange, onCommit, throttleMs = 0, disabled = false, keyDownLabel, keyUpLabel, className = '', labelRowClass = '', labelClass = '', valueClass = '', sliderClass = '', endpointClass = '', endpointLabelClass = '', accentClass = '', showValue = true, showEndpoints, }) { const isVertical = orientation === 'vertical'; const defaultShowEndpoints = !isVertical; const shouldShowEndpoints = typeof showEndpoints === 'boolean' ? showEndpoints : defaultShowEndpoints; const [pending, setPending] = useState(value); const draggingRef = useRef(false); const throttleRef = useRef(null); useEffect(() => { if (!draggingRef.current) { setPending(value); } }, [value]); useEffect( () => () => { if (throttleRef.current) { clearTimeout(throttleRef.current); } }, [], ); const scheduleSend = (next) => { if (!onChange) return; if (throttleMs > 0) { if (throttleRef.current) { clearTimeout(throttleRef.current); } throttleRef.current = setTimeout(() => { onChange(next); }, throttleMs); return; } onChange(next); }; const handleSlider = (event) => { const next = Number.parseFloat(event.target.value); if (Number.isNaN(next)) return; draggingRef.current = true; setPending(next); scheduleSend(next); }; const commitSlider = () => { draggingRef.current = false; if (throttleRef.current) { clearTimeout(throttleRef.current); throttleRef.current = null; } if (onCommit) { onCommit(pending); } else if (throttleMs > 0) { onChange?.(pending); } }; if (typeof min !== 'number' || typeof max !== 'number') return null; const mergedSliderClass = [ isVertical ? 'h-28 w-5' : 'w-full', accentClass, sliderClass, ] .filter(Boolean) .join(' '); const slider = ( ); if (isVertical) { return (