summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/timeline.tsx
diff options
context:
space:
mode:
authorPulkit Garg <pulkitgargbnl@gmail.com>2025-06-24 10:28:58 +0530
committerPulkit Garg <pulkitgargbnl@gmail.com>2025-06-24 10:28:58 +0530
commite664ea0271a9c34bb7290447823367ba6fffac7d (patch)
treeecbf5d1840edf21d661183f6ecba53ac55d73ad2 /apps/web/src/components/editor/timeline.tsx
parent4260d4be08817393af96e7843ed828183137119f (diff)
downloadopencut-e664ea0271a9c34bb7290447823367ba6fffac7d.tar.gz
feat: Enable Playhead Dragging for Video Navigation
Diffstat (limited to 'apps/web/src/components/editor/timeline.tsx')
-rw-r--r--apps/web/src/components/editor/timeline.tsx67
1 files changed, 55 insertions, 12 deletions
diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx
index 675b398c..e97b9cec 100644
--- a/apps/web/src/components/editor/timeline.tsx
+++ b/apps/web/src/components/editor/timeline.tsx
@@ -27,7 +27,7 @@ import { useMediaStore } from "@/stores/media-store";
import { usePlaybackStore } from "@/stores/playback-store";
import { processMediaFiles } from "@/lib/media-processing";
import { toast } from "sonner";
-import { useState, useRef, useEffect } from "react";
+import { useState, useRef, useEffect, useCallback } from "react";
import {
Select,
SelectContent,
@@ -69,6 +69,10 @@ export function Timeline() {
additive: boolean;
} | null>(null);
+ // Playhead scrubbing state
+ const [isScrubbing, setIsScrubbing] = useState(false);
+ const [scrubTime, setScrubTime] = useState<number | null>(null);
+
// Update timeline duration when tracks change
useEffect(() => {
const totalDuration = getTotalDuration();
@@ -304,6 +308,41 @@ export function Timeline() {
setZoomLevel((prev) => Math.max(0.1, Math.min(10, prev + delta)));
};
+ // --- Playhead Scrubbing Handlers ---
+ const handlePlayheadMouseDown = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ setIsScrubbing(true);
+ handleScrub(e);
+ }, [duration, zoomLevel]);
+
+ const handleScrub = useCallback((e: MouseEvent | React.MouseEvent) => {
+ const timeline = timelineRef.current;
+ if (!timeline) return;
+ const rect = timeline.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const time = Math.max(0, Math.min(duration, x / (50 * zoomLevel)));
+ setScrubTime(time);
+ seek(time); // update video preview in real time
+ }, [duration, zoomLevel, seek]);
+
+ useEffect(() => {
+ if (!isScrubbing) return;
+ const onMouseMove = (e: MouseEvent) => handleScrub(e);
+ const onMouseUp = (e: MouseEvent) => {
+ setIsScrubbing(false);
+ if (scrubTime !== null) seek(scrubTime); // finalize seek
+ setScrubTime(null);
+ };
+ window.addEventListener("mousemove", onMouseMove);
+ window.addEventListener("mouseup", onMouseUp);
+ return () => {
+ window.removeEventListener("mousemove", onMouseMove);
+ window.removeEventListener("mouseup", onMouseUp);
+ };
+ }, [isScrubbing, scrubTime, seek, handleScrub]);
+
+ const playheadPosition = isScrubbing && scrubTime !== null ? scrubTime : currentTime;
+
const dragProps = {
onDragEnter: handleDragEnter,
onDragOver: handleDragOver,
@@ -555,10 +594,11 @@ export function Timeline() {
}).filter(Boolean);
})()}
- {/* Playhead in ruler */}
+ {/* Playhead in ruler (scrubbable) */}
<div
- className="absolute top-0 bottom-0 w-0.5 bg-red-500 pointer-events-none z-10"
- style={{ left: `${currentTime * 50 * zoomLevel}px` }}
+ className="absolute top-0 bottom-0 w-0.5 bg-red-500 pointer-events-auto z-10 cursor-ew-resize"
+ style={{ left: `${playheadPosition * 50 * zoomLevel}px` }}
+ onMouseDown={handlePlayheadMouseDown}
>
<div className="absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 bg-red-500 rounded-full border-2 border-white shadow-sm" />
</div>
@@ -668,14 +708,17 @@ export function Timeline() {
</div>
))}
- {/* Playhead for tracks area */}
- <div
- className="absolute top-0 w-0.5 bg-red-500 pointer-events-none z-20"
- style={{
- left: `${currentTime * 50 * zoomLevel}px`,
- height: `${tracks.length * 60}px`,
- }}
- />
+ {/* Playhead for tracks area (scrubbable) */}
+ {tracks.length > 0 && (
+ <div
+ className="absolute top-0 w-0.5 bg-red-500 pointer-events-auto z-20 cursor-ew-resize"
+ style={{
+ left: `${playheadPosition * 50 * zoomLevel}px`,
+ height: `${tracks.length * 60}px`,
+ }}
+ onMouseDown={handlePlayheadMouseDown}
+ />
+ )}
</>
)}
</div>