diff --git a/src/components/TrimControl.tsx b/src/components/TrimControl.tsx index a4a03d19..9c12af57 100644 --- a/src/components/TrimControl.tsx +++ b/src/components/TrimControl.tsx @@ -1,7 +1,7 @@ "use client"; import { EditRecipe } from "@/lib/types"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { AlertCircle } from "lucide-react"; import { formatDuration } from "@/lib/utils"; @@ -16,13 +16,56 @@ export default function TrimControl({ recipe, onChange, duration }: Props) { const [invalidEnd, setEnd] = useState(false); const [startErrorMsg, setStartErrorMsg] = useState(""); const [endErrorMsg, setEndErrorMsg] = useState(""); - const [startInput, setStartInput] = useState(recipe.trimStart.toString()); + const [startInput, setStartInput] = useState( + recipe.trimStart.toString() + ); useEffect(() => { setStartInput(recipe.trimStart.toString()); }, [recipe.trimStart]); - const clipLength = (recipe.trimEnd ?? duration) - recipe.trimStart; + const clipLength = + (recipe.trimEnd ?? duration) - recipe.trimStart; + + const trackRef = useRef(null); + const dragging = useRef<"start" | "end" | null>(null); + + const xToSeconds = useCallback((clientX: number) => { + const track = trackRef.current; + if (!track || duration <= 0) return 0; + const { left, width } = track.getBoundingClientRect(); + const ratio = Math.max(0, Math.min(1, (clientX - left) / width)); + return parseFloat((ratio * duration).toFixed(1)); + }, [duration]); + + const applyDrag = useCallback((clientX: number) => { + const seconds = xToSeconds(clientX); + if (dragging.current === "start") { + const clamped = Math.min(seconds, (recipe.trimEnd ?? duration) - 0.1); + onChange({ trimStart: Math.max(0, clamped) }); + } else if (dragging.current === "end") { + const clamped = Math.max(seconds, recipe.trimStart + 0.1); + onChange({ trimEnd: Math.min(duration, clamped) }); + } + }, [xToSeconds, duration, recipe.trimStart, recipe.trimEnd, onChange]); + + useEffect(() => { + const onMove = (e: MouseEvent | TouchEvent) => { + const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; + applyDrag(clientX); + }; + const onUp = () => { dragging.current = null; }; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + document.addEventListener("touchmove", onMove); + document.addEventListener("touchend", onUp); + return () => { + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + document.removeEventListener("touchmove", onMove); + document.removeEventListener("touchend", onUp); + }; + }, [applyDrag]); const handleStart = (val: string) => { setStartInput(val); @@ -50,7 +93,7 @@ export default function TrimControl({ recipe, onChange, duration }: Props) { if (duration > 0 && n >= duration) { setStart(true); setStartErrorMsg( - `Start time must be less than duration (${duration.toFixed(1)}s).`, + `Start time must be less than duration (${duration.toFixed(1)}s).` ); return; } @@ -113,6 +156,64 @@ export default function TrimControl({ recipe, onChange, duration }: Props) { return (
+ {duration > 0 && ( +
{ + if (dragging.current) return; + const s = xToSeconds(e.clientX); + onChange({ trimStart: s }); + }} + onKeyDown={(e) => { + if (e.key === "ArrowLeft") onChange({ trimStart: Math.max(0, recipe.trimStart - 0.1) }); + if (e.key === "ArrowRight") onChange({ trimStart: Math.min((recipe.trimEnd ?? duration) - 0.1, recipe.trimStart + 0.1) }); + }} + > +
+
+
{ dragging.current = "start"; }} + onTouchStart={() => { dragging.current = "start"; }} + onKeyDown={(e) => { + if (e.key === "ArrowLeft") onChange({ trimStart: Math.max(0, recipe.trimStart - 0.1) }); + if (e.key === "ArrowRight") onChange({ trimStart: Math.min((recipe.trimEnd ?? duration) - 0.1, recipe.trimStart + 0.1) }); + }} + /> +
{ dragging.current = "end"; }} + onTouchStart={() => { dragging.current = "end"; }} + onKeyDown={(e) => { + if (e.key === "ArrowLeft") onChange({ trimEnd: Math.max(recipe.trimStart + 0.1, (recipe.trimEnd ?? duration) - 0.1) }); + if (e.key === "ArrowRight") onChange({ trimEnd: Math.min(duration, (recipe.trimEnd ?? duration) + 0.1) }); + }} + /> +
+ )}
); } + +