diff --git a/src/components/video-editor/SliderControl.tsx b/src/components/video-editor/SliderControl.tsx index d9db8998..dd0aaf0d 100644 --- a/src/components/video-editor/SliderControl.tsx +++ b/src/components/video-editor/SliderControl.tsx @@ -1,5 +1,5 @@ import type { PointerEvent as ReactPointerEvent } from "react"; -import { useCallback, useRef } from "react"; +import { useCallback, useRef, memo, useEffect } from "react"; import { cn } from "@/lib/utils"; interface SliderControlProps { @@ -27,7 +27,7 @@ function quantizeToStep(value: number, min: number, step: number) { return min + Math.round((value - min) / step) * step; } -export function SliderControl({ +export const SliderControl = memo(function SliderControl({ label, value, defaultValue: _defaultValue, @@ -40,30 +40,51 @@ export function SliderControl({ accentColor = "blue", }: SliderControlProps) { const rootRef = useRef(null); + const valueTextRef = useRef(null); + const boundsRef = useRef(null); + const requestRef = useRef(null); + const pct = Math.min(100, Math.max(0, ((value - min) / (max - min || 1)) * 100)); + const dividerClass = accentColor === "purple" ? "bg-foreground/95 shadow-[0_0_10px_rgba(139,92,246,0.28)]" : "bg-foreground/95 shadow-[0_0_10px_rgba(37,99,235,0.28)]"; - const setValueFromClientX = useCallback( + // Sync initial and prop-driven changes to CSS variable + useEffect(() => { + if (rootRef.current) { + rootRef.current.style.setProperty("--slider-pct", String(pct / 100)); + } + }, [pct]); + + const updateValue = useCallback( (clientX: number) => { - const root = rootRef.current; - if (!root) { + const bounds = boundsRef.current; + if (!bounds || bounds.width <= 6) { return; } - const bounds = root.getBoundingClientRect(); - if (!(bounds.width > 0)) { - return; - } - - const normalized = clamp((clientX - bounds.left) / bounds.width, 0, 1); + const normalized = clamp((clientX - (bounds.left + 3)) / (bounds.width - 6), 0, 1); const rawValue = min + normalized * (max - min); const nextValue = clamp(quantizeToStep(rawValue, min, step), min, max); - onChange(Number(nextValue.toFixed(6))); + const finalValue = Number(nextValue.toFixed(6)); + const finalPct = (((finalValue - min) / (max - min || 1)) * 100).toFixed(4); + + // Direct DOM update for instant feedback + if (rootRef.current) { + rootRef.current.style.setProperty("--slider-pct", String(Number(finalPct) / 100)); + rootRef.current.setAttribute("aria-valuenow", String(finalValue)); + rootRef.current.setAttribute("aria-valuetext", formatValue(finalValue)); + } + if (valueTextRef.current) { + valueTextRef.current.textContent = formatValue(finalValue); + } + + // Notify parent + onChange(finalValue); }, - [max, min, onChange, step], + [max, min, onChange, step, formatValue], ); const handlePointerDown = useCallback( @@ -72,15 +93,24 @@ export function SliderControl({ const pointerId = event.pointerId; const target = event.currentTarget; + // Cache bounds to avoid layout thrashing during move + boundsRef.current = target.getBoundingClientRect(); + target.setPointerCapture(pointerId); - setValueFromClientX(event.clientX); + updateValue(event.clientX); const handlePointerMove = (moveEvent: PointerEvent) => { if (moveEvent.pointerId !== pointerId) { return; } - setValueFromClientX(moveEvent.clientX); + if (requestRef.current) { + cancelAnimationFrame(requestRef.current); + } + + requestRef.current = requestAnimationFrame(() => { + updateValue(moveEvent.clientX); + }); }; const finishPointer = (finishEvent: PointerEvent) => { @@ -88,17 +118,27 @@ export function SliderControl({ return; } + if (requestRef.current) { + cancelAnimationFrame(requestRef.current); + requestRef.current = null; + } + + if (finishEvent.type === "pointerup") { + updateValue(finishEvent.clientX); + } + target.releasePointerCapture(pointerId); target.removeEventListener("pointermove", handlePointerMove); target.removeEventListener("pointerup", finishPointer); target.removeEventListener("pointercancel", finishPointer); + boundsRef.current = null; }; target.addEventListener("pointermove", handlePointerMove); target.addEventListener("pointerup", finishPointer); target.addEventListener("pointercancel", finishPointer); }, - [setValueFromClientX], + [updateValue], ); return ( @@ -124,11 +164,16 @@ export function SliderControl({ } }} className="relative flex h-10 w-full select-none items-center overflow-hidden rounded-xl bg-editor-bg/80 px-1.5 outline-none focus-visible:ring-1 focus-visible:ring-[#2563EB]/40" + style={ + { + "--slider-pct": String(pct / 100), + } as React.CSSProperties + } >
0 ? `max(calc(${pct}% - 6px), 2.1rem)` : 0, + width: "calc(var(--slider-pct) * (100% - 6px))", }} />
{label} - + {formatValue(value)}
); -} +});