diff --git a/apps/web/src/components/editor/timeline-toolbar.tsx b/apps/web/src/components/editor/timeline-toolbar.tsx
new file mode 100644
index 0000000..5146479
--- /dev/null
+++ b/apps/web/src/components/editor/timeline-toolbar.tsx
@@ -0,0 +1,219 @@
+"use client";
+
+import type { TrackType } from "@/types/timeline";
+import {
+ ArrowLeftToLine,
+ ArrowRightToLine,
+ Copy,
+ Pause,
+ Play,
+ Scissors,
+ Snowflake,
+ SplitSquareHorizontal,
+ Trash2,
+} from "lucide-react";
+import { Button } from "../ui/button";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "../ui/select";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "../ui/tooltip";
+
+interface TimelineToolbarProps {
+ isPlaying: boolean;
+ currentTime: number;
+ duration: number;
+ speed: number;
+ tracks: any[];
+ toggle: () => void;
+ setSpeed: (speed: number) => void;
+ addTrack: (type: TrackType) => string;
+ addClipToTrack: (trackId: string, clip: any) => void;
+ handleSplitSelected: () => void;
+ handleDuplicateSelected: () => void;
+ handleFreezeSelected: () => void;
+ handleDeleteSelected: () => void;
+}
+
+export function TimelineToolbar({
+ isPlaying,
+ currentTime,
+ duration,
+ speed,
+ tracks,
+ toggle,
+ setSpeed,
+ addTrack,
+ addClipToTrack,
+ handleSplitSelected,
+ handleDuplicateSelected,
+ handleFreezeSelected,
+ handleDeleteSelected,
+}: TimelineToolbarProps) {
+ return (
+
+
+ {/* Play/Pause Button */}
+
+
+
+
+
+ {isPlaying ? "Pause (Space)" : "Play (Space)"}
+
+
+
+
+
+ {/* Time Display */}
+
+ {currentTime.toFixed(1)}s / {duration.toFixed(1)}s
+
+
+ {/* Test Clip Button - for debugging */}
+ {tracks.length === 0 && (
+ <>
+
+
+
+
+
+ Add a test clip to try playback
+
+ >
+ )}
+
+
+
+
+
+
+
+ Split clip (S)
+
+
+
+
+
+
+ Split and keep left (A)
+
+
+
+
+
+
+ Split and keep right (D)
+
+
+
+
+
+
+ Separate audio (E)
+
+
+
+
+
+
+ Duplicate clip (Ctrl+D)
+
+
+
+
+
+
+ Freeze frame (F)
+
+
+
+
+
+
+ Delete clip (Delete)
+
+
+
+
+ {/* Speed Control */}
+
+
+
+
+ Playback Speed
+
+
+
+ );
+}
diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx
index ae6b98d..4de290a 100644
--- a/apps/web/src/components/editor/timeline.tsx
+++ b/apps/web/src/components/editor/timeline.tsx
@@ -5,14 +5,9 @@ import { useMediaStore } from "@/stores/media-store";
import { usePlaybackStore } from "@/stores/playback-store";
import { useTimelineStore, type TimelineTrack } from "@/stores/timeline-store";
import {
- ArrowLeftToLine,
- ArrowRightToLine,
Copy,
MoreVertical,
- Pause,
- Play,
Scissors,
- Snowflake,
SplitSquareHorizontal,
Trash2,
Volume2,
@@ -22,23 +17,9 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "../ui/select";
-
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "../ui/tooltip";
import AudioWaveform from "./audio-waveform";
-
+import { TimelineToolbar } from "./timeline-toolbar";
export function Timeline() {
// Timeline shows all tracks (video, audio, effects) and their clips.
@@ -71,7 +52,6 @@ export function Timeline() {
speed,
} = usePlaybackStore();
const [isDragOver, setIsDragOver] = useState(false);
- const [isProcessing, setIsProcessing] = useState(false);
const [zoomLevel, setZoomLevel] = useState(1);
const dragCounterRef = useRef(0);
const timelineRef = useRef(null);
@@ -217,7 +197,7 @@ export function Timeline() {
const bx2 = clamp(x2, 0, rect.width);
const by1 = clamp(y1, 0, rect.height);
const by2 = clamp(y2, 0, rect.height);
- let newSelection: { trackId: string; clipId: string; }[] = [];
+ let newSelection: { trackId: string; clipId: string }[] = [];
tracks.forEach((track, trackIdx) => {
track.clips.forEach((clip) => {
const effectiveDuration = clip.duration - clip.trimStart - clip.trimEnd;
@@ -335,8 +315,6 @@ export function Timeline() {
toast.error("Failed to add media to timeline");
}
} else if (e.dataTransfer.files?.length > 0) {
- // Handle file drops by creating new tracks
- setIsProcessing(true);
try {
const processedItems = await processMediaFiles(e.dataTransfer.files);
for (const processedItem of processedItems) {
@@ -364,8 +342,6 @@ export function Timeline() {
// Show error if file processing fails
console.error("Error processing external files:", error);
toast.error("Failed to process dropped files");
- } finally {
- setIsProcessing(false);
}
}
};
@@ -570,162 +546,21 @@ export function Timeline() {
onMouseLeave={() => setIsInTimeline(false)}
onWheel={handleWheel}
>
- {/* Toolbar */}
-
-
- {/* Play/Pause Button */}
-
-
-
-
-
- {isPlaying ? "Pause (Space)" : "Play (Space)"}
-
-
-
-
-
- {/* Time Display */}
-
- {currentTime.toFixed(1)}s / {duration.toFixed(1)}s
-
-
- {/* Test Clip Button - for debugging */}
- {tracks.length === 0 && (
- <>
-
-
-
-
-
- Add a test clip to try playback
-
- >
- )}
-
-
-
-
-
-
-
- Split clip (S)
-
-
-
-
-
-
- Split and keep left (A)
-
-
-
-
-
-
- Split and keep right (D)
-
-
-
-
-
-
- Separate audio (E)
-
-
-
-
-
-
- Duplicate clip (Ctrl+D)
-
-
-
-
-
-
- Freeze frame (F)
-
-
-
-
-
-
- Delete clip (Delete)
-
-
-
-
- {/* Speed Control */}
-
-
-
-
- Playback Speed
-
-
-
+
{/* Timeline Container */}
@@ -781,17 +616,19 @@ export function Timeline() {
return (
{(() => {
const formatTime = (seconds: number) => {
@@ -852,12 +689,13 @@ export function Timeline() {
>
{track.name}
@@ -1197,7 +1035,7 @@ function TimelineTrackContent({
});
};
- const updateTrimFromMouseMove = (e: { clientX: number; }) => {
+ const updateTrimFromMouseMove = (e: { clientX: number }) => {
if (!resizing) return;
const clip = track.clips.find((c) => c.id === resizing.clipId);
@@ -1632,18 +1470,18 @@ function TimelineTrackContent({
}
if (mediaItem.type === "audio") {
- return (
-
-
- );
- }
+ );
+ }
// Fallback for videos without thumbnails
return (
@@ -1681,12 +1519,13 @@ function TimelineTrackContent({
return (
{
e.preventDefault();
// Only show track menu if we didn't click on a clip
@@ -1710,12 +1549,13 @@ function TimelineTrackContent({
{track.clips.length === 0 ? (
{isDropping
? wouldOverlap
@@ -1860,4 +1700,3 @@ function TimelineTrackContent({
);
}
-
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts
index 3b068d7..4018c30 100644
--- a/apps/web/src/stores/timeline-store.ts
+++ b/apps/web/src/stores/timeline-store.ts
@@ -1,4 +1,5 @@
import { create } from "zustand";
+import type { TrackType } from "@/types/timeline";
export interface TimelineClip {
id: string;
@@ -13,7 +14,7 @@ export interface TimelineClip {
export interface TimelineTrack {
id: string;
name: string;
- type: "video" | "audio" | "effects";
+ type: TrackType;
clips: TimelineClip[];
muted?: boolean;
}
@@ -52,7 +53,7 @@ interface TimelineStore {
endDrag: () => void;
// Actions
- addTrack: (type: "video" | "audio" | "effects") => string;
+ addTrack: (type: TrackType) => string;
removeTrack: (trackId: string) => void;
addClipToTrack: (trackId: string, clip: Omit
) => void;
removeClipFromTrack: (trackId: string, clipId: string) => void;
diff --git a/apps/web/src/types/timeline.ts b/apps/web/src/types/timeline.ts
index 21be520..7fafc18 100644
--- a/apps/web/src/types/timeline.ts
+++ b/apps/web/src/types/timeline.ts
@@ -1,5 +1,7 @@
import { TimelineTrack, TimelineClip } from "@/stores/timeline-store";
+export type TrackType = "video" | "audio" | "effects";
+
export interface TimelineClipProps {
clip: TimelineClip;
track: TimelineTrack;