diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx
index 987215fa..3ca561c5 100644
--- a/apps/web/src/components/editor/timeline/index.tsx
+++ b/apps/web/src/components/editor/timeline/index.tsx
@@ -64,23 +64,11 @@ export function Timeline() {
const {
tracks,
- addTrack,
- addElementToTrack,
- removeElementFromTrack,
- removeElementFromTrackWithRipple,
getTotalDuration,
- selectedElements,
clearSelectedElements,
- setSelectedElements,
- splitElement,
- splitAndKeepLeft,
- splitAndKeepRight,
- toggleTrackMute,
- separateAudio,
snappingEnabled,
- toggleSnapping,
- rippleEditingEnabled,
- toggleRippleEditing,
+ setSelectedElements,
+ toggleTrackMute,
dragState,
} = useTimelineStore();
const { mediaItems, addMediaItem } = useMediaStore();
@@ -430,139 +418,6 @@ export function Timeline() {
onDrop: handleDrop,
};
- // Action handlers for toolbar
- const handleSplitSelected = () => {
- if (selectedElements.length === 0) {
- return;
- }
- let splitCount = 0;
- selectedElements.forEach(({ trackId, elementId }) => {
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
- if (element && track) {
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (currentTime > effectiveStart && currentTime < effectiveEnd) {
- const newElementId = splitElement(trackId, elementId, currentTime);
- if (newElementId) splitCount++;
- }
- }
- });
- if (splitCount === 0) {
- toast.error("Playhead must be within selected elements to split");
- }
- };
-
- const handleDuplicateSelected = () => {
- if (selectedElements.length === 0) {
- return;
- }
- const canDuplicate = selectedElements.length === 1;
- if (!canDuplicate) return;
-
- const newSelections: { trackId: string; elementId: string }[] = [];
-
- selectedElements.forEach(({ trackId, elementId }) => {
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((el) => el.id === elementId);
-
- if (element) {
- const newStartTime =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd) +
- 0.1;
-
- // Create element without id (will be generated by store)
- const { id, ...elementWithoutId } = element;
-
- addElementToTrack(trackId, {
- ...elementWithoutId,
- startTime: newStartTime,
- });
-
- // We can't predict the new id, so just clear selection for now
- // TODO: addElementToTrack could return the new element id
- }
- });
-
- clearSelectedElements();
- };
-
- const handleFreezeSelected = () => {
- toast.info("Freeze frame functionality coming soon!");
- };
-
- const handleSplitAndKeepLeft = () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element");
- return;
- }
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
- if (!element) return;
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
- if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
- toast.error("Playhead must be within selected element");
- return;
- }
- splitAndKeepLeft(trackId, elementId, currentTime);
- };
-
- const handleSplitAndKeepRight = () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element");
- return;
- }
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
- if (!element) return;
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
- if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
- toast.error("Playhead must be within selected element");
- return;
- }
- splitAndKeepRight(trackId, elementId, currentTime);
- };
-
- const handleSeparateAudio = () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one media element to separate audio");
- return;
- }
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t) => t.id === trackId);
- if (!track || track.type !== "media") {
- toast.error("Select a media element to separate audio");
- return;
- }
- separateAudio(trackId, elementId);
- };
-
- const handleDeleteSelected = () => {
- if (selectedElements.length === 0) {
- return;
- }
- selectedElements.forEach(({ trackId, elementId }) => {
- if (rippleEditingEnabled) {
- removeElementFromTrackWithRipple(trackId, elementId);
- } else {
- removeElementFromTrack(trackId, elementId);
- }
- });
- clearSelectedElements();
- };
-
// --- Scroll synchronization effect ---
useEffect(() => {
const rulerViewport = rulerScrollRef.current?.querySelector(
@@ -649,192 +504,7 @@ export function Timeline() {
onMouseEnter={() => setIsInTimeline(true)}
onMouseLeave={() => setIsInTimeline(false)}
>
- {/* 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 element (Ctrl+S)
-
-
-
-
-
- Split and keep left (Ctrl+Q)
-
-
-
-
-
- Split and keep right (Ctrl+W)
-
-
-
-
-
- Separate audio (Ctrl+D)
-
-
-
-
-
- Duplicate element (Ctrl+D)
-
-
-
-
-
- Freeze frame (F)
-
-
-
-
-
- Delete element (Delete)
-
-
-
-
-
-
-
-
-
- Auto snapping
-
-
-
-
-
-
- {rippleEditingEnabled
- ? "Disable Ripple Editing"
- : "Enable Ripple Editing"}
-
-
-
-
-
+
{/* Timeline Container */}
);
}
+
+function TimelineToolbar() {
+ const {
+ tracks,
+ addTrack,
+ addElementToTrack,
+ removeElementFromTrack,
+ removeElementFromTrackWithRipple,
+ selectedElements,
+ clearSelectedElements,
+ splitElement,
+ splitAndKeepLeft,
+ splitAndKeepRight,
+ separateAudio,
+ snappingEnabled,
+ toggleSnapping,
+ rippleEditingEnabled,
+ toggleRippleEditing,
+ } = useTimelineStore();
+ const { currentTime, duration, isPlaying, toggle } = usePlaybackStore();
+ const { activeProject } = useProjectStore();
+ const { mediaItems, addMediaItem } = useMediaStore();
+
+ // Action handlers
+ const handleSplitSelected = () => {
+ if (selectedElements.length === 0) return;
+ let splitCount = 0;
+ selectedElements.forEach(({ trackId, elementId }) => {
+ const track = tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((c) => c.id === elementId);
+ if (element && track) {
+ const effectiveStart = element.startTime;
+ const effectiveEnd =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
+ if (currentTime > effectiveStart && currentTime < effectiveEnd) {
+ const newElementId = splitElement(trackId, elementId, currentTime);
+ if (newElementId) splitCount++;
+ }
+ }
+ });
+ if (splitCount === 0) {
+ toast.error("Playhead must be within selected elements to split");
+ }
+ };
+
+ const handleDuplicateSelected = () => {
+ if (selectedElements.length === 0) return;
+ const canDuplicate = selectedElements.length === 1;
+ if (!canDuplicate) return;
+
+ selectedElements.forEach(({ trackId, elementId }) => {
+ const track = tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((el) => el.id === elementId);
+ if (element) {
+ const newStartTime =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd) +
+ 0.1;
+ const { id, ...elementWithoutId } = element;
+ addElementToTrack(trackId, {
+ ...elementWithoutId,
+ startTime: newStartTime,
+ });
+ }
+ });
+ clearSelectedElements();
+ };
+
+ const handleFreezeSelected = () => {
+ toast.info("Freeze frame functionality coming soon!");
+ };
+
+ const handleSplitAndKeepLeft = () => {
+ if (selectedElements.length !== 1) {
+ toast.error("Select exactly one element");
+ return;
+ }
+ const { trackId, elementId } = selectedElements[0];
+ const track = tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((c) => c.id === elementId);
+ if (!element) return;
+ const effectiveStart = element.startTime;
+ const effectiveEnd =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
+ if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
+ toast.error("Playhead must be within selected element");
+ return;
+ }
+ splitAndKeepLeft(trackId, elementId, currentTime);
+ };
+
+ const handleSplitAndKeepRight = () => {
+ if (selectedElements.length !== 1) {
+ toast.error("Select exactly one element");
+ return;
+ }
+ const { trackId, elementId } = selectedElements[0];
+ const track = tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((c) => c.id === elementId);
+ if (!element) return;
+ const effectiveStart = element.startTime;
+ const effectiveEnd =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
+ if (currentTime <= effectiveStart || currentTime >= effectiveEnd) {
+ toast.error("Playhead must be within selected element");
+ return;
+ }
+ splitAndKeepRight(trackId, elementId, currentTime);
+ };
+
+ const handleSeparateAudio = () => {
+ if (selectedElements.length !== 1) {
+ toast.error("Select exactly one media element to separate audio");
+ return;
+ }
+ const { trackId, elementId } = selectedElements[0];
+ const track = tracks.find((t) => t.id === trackId);
+ if (!track || track.type !== "media") {
+ toast.error("Select a media element to separate audio");
+ return;
+ }
+ separateAudio(trackId, elementId);
+ };
+
+ const handleDeleteSelected = () => {
+ if (selectedElements.length === 0) return;
+ selectedElements.forEach(({ trackId, elementId }) => {
+ if (rippleEditingEnabled) {
+ removeElementFromTrackWithRipple(trackId, elementId);
+ } else {
+ removeElementFromTrack(trackId, elementId);
+ }
+ });
+ clearSelectedElements();
+ };
+ 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 element (Ctrl+S)
+
+
+
+
+
+ Split and keep left (Ctrl+Q)
+
+
+
+
+
+ Split and keep right (Ctrl+W)
+
+
+
+
+
+ Separate audio (Ctrl+D)
+
+
+
+
+
+ Duplicate element (Ctrl+D)
+
+
+
+
+
+ Freeze frame (F)
+
+
+
+
+
+ Delete element (Delete)
+
+
+
+
+
+
+
+
+
+ Auto snapping
+
+
+
+
+
+
+ {rippleEditingEnabled
+ ? "Disable Ripple Editing"
+ : "Enable Ripple Editing"}
+
+
+
+
+
+ );
+}