From 4de81796a34f43d40e188f11f298943bb0654972 Mon Sep 17 00:00:00 2001 From: Anwarul Islam Date: Tue, 15 Jul 2025 04:23:45 +0600 Subject: feat: add SnapIndicator component for timeline snapping visualization --- apps/web/src/components/editor/snap-indicator.tsx | 75 +++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 apps/web/src/components/editor/snap-indicator.tsx (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/snap-indicator.tsx b/apps/web/src/components/editor/snap-indicator.tsx new file mode 100644 index 00000000..398e06c6 --- /dev/null +++ b/apps/web/src/components/editor/snap-indicator.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { SnapPoint } from "@/hooks/use-timeline-snapping"; +import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; + +interface SnapIndicatorProps { + snapPoint: SnapPoint | null; + zoomLevel: number; + timelineHeight: number; + isVisible: boolean; +} + +export function SnapIndicator({ + snapPoint, + zoomLevel, + timelineHeight, + isVisible, +}: SnapIndicatorProps) { + if (!isVisible || !snapPoint) { + return null; + } + + const leftPosition = + snapPoint.time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; + + const getIndicatorColor = () => { + switch (snapPoint.type) { + case "grid": + return "bg-blue-400"; + case "element-start": + case "element-end": + return "bg-green-400"; + case "playhead": + return "bg-red-400"; + default: + return "bg-gray-400"; + } + }; + + const getIndicatorLabel = () => { + switch (snapPoint.type) { + case "grid": + return "Grid"; + case "element-start": + return "Start"; + case "element-end": + return "End"; + case "playhead": + return "Playhead"; + default: + return ""; + } + }; + + return ( +
+ {/* Snap line */} +
+ + {/* Snap label */} +
+ {getIndicatorLabel()} ({snapPoint.time.toFixed(1)}s) +
+
+ ); +} -- cgit v1.3.1 From af632d8cc5100124ebd12ba0173d01ba651cccd8 Mon Sep 17 00:00:00 2001 From: Anwarul Islam Date: Tue, 15 Jul 2025 04:27:12 +0600 Subject: feat: add snapping functionality and controls to timeline component --- apps/web/src/components/editor/timeline.tsx | 136 ++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 71e2997b..cf139aa2 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -15,6 +15,10 @@ import { Video, Music, TypeIcon, + Magnet, + Grid3X3, + Move, + Target, } from "lucide-react"; import { Tooltip, @@ -50,6 +54,8 @@ import { } from "./timeline-playhead"; import { SelectionBox } from "./selection-box"; import { useSelectionBox } from "@/hooks/use-selection-box"; +import { SnapIndicator } from "./snap-indicator"; +import { useTimelineSnapping } from "@/hooks/use-timeline-snapping"; import type { DragData, TimelineTrack } from "@/types/timeline"; import { getTrackHeight, @@ -78,6 +84,17 @@ export function Timeline() { separateAudio, undo, redo, + snappingEnabled, + gridSnappingEnabled, + elementSnappingEnabled, + playheadSnappingEnabled, + snapThreshold, + gridInterval, + toggleSnapping, + toggleGridSnapping, + toggleElementSnapping, + togglePlayheadSnapping, + dragState, } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { activeProject } = useProjectStore(); @@ -145,6 +162,20 @@ export function Timeline() { }, }); + // Initialize snapping functionality + const { snapElementPosition } = useTimelineSnapping({ + snapThreshold, + gridInterval, + enableGridSnapping: snappingEnabled && gridSnappingEnabled, + enableElementSnapping: snappingEnabled && elementSnappingEnabled, + enablePlayheadSnapping: snappingEnabled && playheadSnappingEnabled, + }); + + // Calculate snap indicator state + const [currentSnapPoint, setCurrentSnapPoint] = useState(null); + const showSnapIndicator = + dragState.isDragging && snappingEnabled && currentSnapPoint; + // Timeline content click to seek handler const handleTimelineContentClick = useCallback( (e: React.MouseEvent) => { @@ -310,6 +341,34 @@ export function Timeline() { return () => window.removeEventListener("keydown", handleKeyDown); }, [redo]); + // Keyboard shortcuts for snapping + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Don't trigger when typing in input fields or textareas + if ( + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement + ) { + return; + } + + // Only trigger when timeline is focused or mouse is over timeline + if ( + !isInTimeline && + !timelineRef.current?.contains(document.activeElement) + ) { + return; + } + + if (e.key === "s" || e.key === "S") { + e.preventDefault(); + toggleSnapping(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleSnapping, isInTimeline]); + // Old marquee system removed - using new SelectionBox component instead const handleDragEnter = (e: React.DragEvent) => { @@ -815,6 +874,72 @@ export function Timeline() { Delete element (Delete) + +
+ + {/* Snapping Controls */} + + + + + Toggle snapping (S) + + + + + + + Toggle grid snapping + + + + + + + Toggle element snapping + + + + + + + Toggle playhead snapping +
@@ -1033,6 +1158,17 @@ export function Timeline() { ))} )} + + {/* Snap Indicator */} +
-- cgit v1.3.1 From 1106aeeb60b493f85bd70685f066362aa43fcd6e Mon Sep 17 00:00:00 2001 From: Anwarul Islam Date: Tue, 15 Jul 2025 04:27:19 +0600 Subject: feat: integrate snapping functionality into timeline track component --- apps/web/src/components/editor/timeline-track.tsx | 42 ++++++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index 033b79a6..0b29c778 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -22,6 +22,7 @@ import { TIMELINE_CONSTANTS, } from "@/constants/timeline-constants"; import { useProjectStore } from "@/stores/project-store"; +import { useTimelineSnapping } from "@/hooks/use-timeline-snapping"; export function TimelineTrackContent({ track, @@ -45,8 +46,25 @@ export function TimelineTrackContent({ endDrag: endDragAction, clearSelectedElements, insertTrackAt, + snappingEnabled, + gridSnappingEnabled, + elementSnappingEnabled, + playheadSnappingEnabled, + snapThreshold, + gridInterval, } = useTimelineStore(); + const { currentTime } = usePlaybackStore(); + + // Initialize snapping hook + const { snapElementPosition } = useTimelineSnapping({ + snapThreshold, + gridInterval, + enableGridSnapping: snappingEnabled && gridSnappingEnabled, + enableElementSnapping: snappingEnabled && elementSnappingEnabled, + enablePlayheadSnapping: snappingEnabled && playheadSnappingEnabled, + }); + const timelineRef = useRef(null); const [isDropping, setIsDropping] = useState(false); const [dropPosition, setDropPosition] = useState(null); @@ -85,12 +103,26 @@ export function TimelineTrackContent({ mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel) ); const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime); - // Use frame snapping if project has FPS, otherwise use decimal snapping - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - const snappedTime = snapTimeToFrame(adjustedTime, projectFps); - updateDragTime(snappedTime); + // Apply snapping if enabled + let finalTime = adjustedTime; + if (snappingEnabled) { + const snapResult = snapElementPosition( + adjustedTime, + tracks, + currentTime, + zoomLevel, + dragState.elementId || undefined + ); + finalTime = snapResult.snappedTime; + } else { + // Use frame snapping if project has FPS, otherwise use decimal snapping + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + finalTime = snapTimeToFrame(adjustedTime, projectFps); + } + + updateDragTime(finalTime); }; const handleMouseUp = (e: MouseEvent) => { -- cgit v1.3.1 From 59f4098f752ff9f992f622e2984278d7b1ba91a4 Mon Sep 17 00:00:00 2001 From: Anwarul Islam Date: Tue, 15 Jul 2025 04:32:36 +0600 Subject: refactor: remove keyboard shortcuts for better integration later --- apps/web/src/components/editor/timeline.tsx | 28 ---------------------------- 1 file changed, 28 deletions(-) (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index cf139aa2..2f0e3d91 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -341,34 +341,6 @@ export function Timeline() { return () => window.removeEventListener("keydown", handleKeyDown); }, [redo]); - // Keyboard shortcuts for snapping - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Don't trigger when typing in input fields or textareas - if ( - e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement - ) { - return; - } - - // Only trigger when timeline is focused or mouse is over timeline - if ( - !isInTimeline && - !timelineRef.current?.contains(document.activeElement) - ) { - return; - } - - if (e.key === "s" || e.key === "S") { - e.preventDefault(); - toggleSnapping(); - } - }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSnapping, isInTimeline]); - // Old marquee system removed - using new SelectionBox component instead const handleDragEnter = (e: React.DragEvent) => { -- cgit v1.3.1 From 2cc25b70d56c7bd9a9d23b38f1cd64e797139fa1 Mon Sep 17 00:00:00 2001 From: Anwarul Islam Date: Tue, 15 Jul 2025 07:03:36 +0600 Subject: feat: enhance snapping functionality by adding snap point handling in Timeline component --- apps/web/src/components/editor/timeline.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 2f0e3d91..882af66e 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -55,7 +55,7 @@ import { import { SelectionBox } from "./selection-box"; import { useSelectionBox } from "@/hooks/use-selection-box"; import { SnapIndicator } from "./snap-indicator"; -import { useTimelineSnapping } from "@/hooks/use-timeline-snapping"; +import { useTimelineSnapping, SnapPoint } from "@/hooks/use-timeline-snapping"; import type { DragData, TimelineTrack } from "@/types/timeline"; import { getTrackHeight, @@ -172,9 +172,16 @@ export function Timeline() { }); // Calculate snap indicator state - const [currentSnapPoint, setCurrentSnapPoint] = useState(null); + const [currentSnapPoint, setCurrentSnapPoint] = useState( + null + ); const showSnapIndicator = - dragState.isDragging && snappingEnabled && currentSnapPoint; + dragState.isDragging && snappingEnabled && currentSnapPoint !== null; + + // Callback to handle snap point changes from TimelineTrackContent + const handleSnapPointChange = useCallback((snapPoint: SnapPoint | null) => { + setCurrentSnapPoint(snapPoint); + }, []); // Timeline content click to seek handler const handleTimelineContentClick = useCallback( @@ -1113,6 +1120,7 @@ export function Timeline() { -- cgit v1.3.1 From e3cb491a29061bb42dcfa58adec50ae54baeddaa Mon Sep 17 00:00:00 2001 From: Anwarul Islam Date: Tue, 15 Jul 2025 07:06:29 +0600 Subject: feat: add snap point notification to TimelineTrackContent for improved snapping feedback --- apps/web/src/components/editor/timeline-track.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index 0b29c778..aeb6f040 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -22,14 +22,16 @@ import { TIMELINE_CONSTANTS, } from "@/constants/timeline-constants"; import { useProjectStore } from "@/stores/project-store"; -import { useTimelineSnapping } from "@/hooks/use-timeline-snapping"; +import { useTimelineSnapping, SnapPoint } from "@/hooks/use-timeline-snapping"; export function TimelineTrackContent({ track, zoomLevel, + onSnapPointChange, }: { track: TimelineTrack; zoomLevel: number; + onSnapPointChange?: (snapPoint: SnapPoint | null) => void; }) { const { mediaItems } = useMediaStore(); const { @@ -106,6 +108,7 @@ export function TimelineTrackContent({ // Apply snapping if enabled let finalTime = adjustedTime; + let snapPoint = null; if (snappingEnabled) { const snapResult = snapElementPosition( adjustedTime, @@ -115,11 +118,18 @@ export function TimelineTrackContent({ dragState.elementId || undefined ); finalTime = snapResult.snappedTime; + snapPoint = snapResult.snapPoint; + + // Notify parent component about snap point change + onSnapPointChange?.(snapPoint); } else { // Use frame snapping if project has FPS, otherwise use decimal snapping const projectStore = useProjectStore.getState(); const projectFps = projectStore.activeProject?.fps || 30; finalTime = snapTimeToFrame(adjustedTime, projectFps); + + // Clear snap point when not snapping + onSnapPointChange?.(null); } updateDragTime(finalTime); @@ -140,6 +150,8 @@ export function TimelineTrackContent({ dragState.currentTime ); endDragAction(); + // Clear snap point when drag ends + onSnapPointChange?.(null); } return; } @@ -236,6 +248,8 @@ export function TimelineTrackContent({ if (isTrackThatStartedDrag) { endDragAction(); + // Clear snap point when drag ends + onSnapPointChange?.(null); } }; @@ -261,6 +275,7 @@ export function TimelineTrackContent({ endDragAction, selectedElements, selectElement, + onSnapPointChange, ]); const handleElementMouseDown = ( -- cgit v1.3.1 From 1fdf2a051529e40cf7a0857d31a83057b878f08d Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 15 Jul 2025 14:16:54 +0200 Subject: cleanup --- apps/web/src/components/editor/snap-indicator.tsx | 65 ++-- .../src/components/editor/timeline-playhead.tsx | 14 +- apps/web/src/components/editor/timeline-track.tsx | 13 +- apps/web/src/components/editor/timeline.tsx | 412 +++++++++------------ apps/web/src/hooks/use-timeline-snapping.ts | 32 +- apps/web/src/stores/timeline-store.ts | 39 -- 6 files changed, 214 insertions(+), 361 deletions(-) (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/snap-indicator.tsx b/apps/web/src/components/editor/snap-indicator.tsx index 398e06c6..83a1f678 100644 --- a/apps/web/src/components/editor/snap-indicator.tsx +++ b/apps/web/src/components/editor/snap-indicator.tsx @@ -2,74 +2,53 @@ import { SnapPoint } from "@/hooks/use-timeline-snapping"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; +import type { TimelineTrack } from "@/types/timeline"; interface SnapIndicatorProps { snapPoint: SnapPoint | null; zoomLevel: number; - timelineHeight: number; isVisible: boolean; + tracks: TimelineTrack[]; + timelineRef: React.RefObject; + trackLabelsRef?: React.RefObject; } export function SnapIndicator({ snapPoint, zoomLevel, - timelineHeight, isVisible, + tracks, + timelineRef, + trackLabelsRef, }: SnapIndicatorProps) { if (!isVisible || !snapPoint) { return null; } - const leftPosition = - snapPoint.time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; + const timelineContainerHeight = timelineRef.current?.offsetHeight || 400; + const totalHeight = timelineContainerHeight - 8; // 8px padding from edges - const getIndicatorColor = () => { - switch (snapPoint.type) { - case "grid": - return "bg-blue-400"; - case "element-start": - case "element-end": - return "bg-green-400"; - case "playhead": - return "bg-red-400"; - default: - return "bg-gray-400"; - } - }; + // Get dynamic track labels width, fallback to 0 if no tracks or no ref + const trackLabelsWidth = + tracks.length > 0 && trackLabelsRef?.current + ? trackLabelsRef.current.offsetWidth + : 0; - const getIndicatorLabel = () => { - switch (snapPoint.type) { - case "grid": - return "Grid"; - case "element-start": - return "Start"; - case "element-end": - return "End"; - case "playhead": - return "Playhead"; - default: - return ""; - } - }; + const leftPosition = + trackLabelsWidth + + snapPoint.time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; return (
- {/* Snap line */} -
- - {/* Snap label */} -
- {getIndicatorLabel()} ({snapPoint.time.toFixed(1)}s) -
+
); } diff --git a/apps/web/src/components/editor/timeline-playhead.tsx b/apps/web/src/components/editor/timeline-playhead.tsx index ce4ed1ff..78aed3a8 100644 --- a/apps/web/src/components/editor/timeline-playhead.tsx +++ b/apps/web/src/components/editor/timeline-playhead.tsx @@ -20,6 +20,7 @@ interface TimelinePlayheadProps { trackLabelsRef?: React.RefObject; timelineRef: React.RefObject; playheadRef?: React.RefObject; + isSnappingToPlayhead?: boolean; } export function TimelinePlayhead({ @@ -34,6 +35,7 @@ export function TimelinePlayhead({ trackLabelsRef, timelineRef, playheadRef: externalPlayheadRef, + isSnappingToPlayhead = false, }: TimelinePlayheadProps) { const internalPlayheadRef = useRef(null); const playheadRef = externalPlayheadRef || internalPlayheadRef; @@ -73,11 +75,15 @@ export function TimelinePlayhead({ }} onMouseDown={handlePlayheadMouseDown} > - {/* The red line spanning full height */} -
+ {/* The playhead line spanning full height */} +
- {/* Red dot indicator at the top (in ruler area) */} -
+ {/* Playhead dot indicator at the top (in ruler area) */} +
); } diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index aeb6f040..2be143eb 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -49,22 +49,15 @@ export function TimelineTrackContent({ clearSelectedElements, insertTrackAt, snappingEnabled, - gridSnappingEnabled, - elementSnappingEnabled, - playheadSnappingEnabled, - snapThreshold, - gridInterval, } = useTimelineStore(); const { currentTime } = usePlaybackStore(); // Initialize snapping hook const { snapElementPosition } = useTimelineSnapping({ - snapThreshold, - gridInterval, - enableGridSnapping: snappingEnabled && gridSnappingEnabled, - enableElementSnapping: snappingEnabled && elementSnappingEnabled, - enablePlayheadSnapping: snappingEnabled && playheadSnappingEnabled, + snapThreshold: 10, + enableElementSnapping: snappingEnabled, + enablePlayheadSnapping: snappingEnabled, }); const timelineRef = useRef(null); diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 882af66e..e83b64b9 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -16,9 +16,7 @@ import { Music, TypeIcon, Magnet, - Grid3X3, - Move, - Target, + Lock, } from "lucide-react"; import { Tooltip, @@ -40,13 +38,6 @@ import { useTimelineZoom } from "@/hooks/use-timeline-zoom"; import { processMediaFiles } from "@/lib/media-processing"; import { toast } from "sonner"; import { useState, useRef, useEffect, useCallback } from "react"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../ui/select"; import { TimelineTrackContent } from "./timeline-track"; import { TimelinePlayhead, @@ -55,7 +46,7 @@ import { import { SelectionBox } from "./selection-box"; import { useSelectionBox } from "@/hooks/use-selection-box"; import { SnapIndicator } from "./snap-indicator"; -import { useTimelineSnapping, SnapPoint } from "@/hooks/use-timeline-snapping"; +import { SnapPoint } from "@/hooks/use-timeline-snapping"; import type { DragData, TimelineTrack } from "@/types/timeline"; import { getTrackHeight, @@ -85,15 +76,7 @@ export function Timeline() { undo, redo, snappingEnabled, - gridSnappingEnabled, - elementSnappingEnabled, - playheadSnappingEnabled, - snapThreshold, - gridInterval, toggleSnapping, - toggleGridSnapping, - toggleElementSnapping, - togglePlayheadSnapping, dragState, } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); @@ -135,7 +118,7 @@ export function Timeline() { const lastVerticalSync = useRef(0); // Timeline playhead ruler handlers - const { handleRulerMouseDown, isDraggingRuler } = useTimelinePlayheadRuler({ + const { handleRulerMouseDown } = useTimelinePlayheadRuler({ currentTime, duration, zoomLevel, @@ -162,15 +145,6 @@ export function Timeline() { }, }); - // Initialize snapping functionality - const { snapElementPosition } = useTimelineSnapping({ - snapThreshold, - gridInterval, - enableGridSnapping: snappingEnabled && gridSnappingEnabled, - enableElementSnapping: snappingEnabled && elementSnappingEnabled, - enablePlayheadSnapping: snappingEnabled && playheadSnappingEnabled, - }); - // Calculate snap indicator state const [currentSnapPoint, setCurrentSnapPoint] = useState( null @@ -724,202 +698,168 @@ export function Timeline() { 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) - - -
- - {/* Snapping Controls */} - - - - - Toggle snapping (S) - - - - - - - Toggle grid snapping - - - - - - - Toggle element snapping - - - - - - - Toggle playhead snapping - - +
+
+ + {/* 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 + + +
{/* Timeline Container */} @@ -939,6 +879,17 @@ export function Timeline() { trackLabelsRef={trackLabelsRef} timelineRef={timelineRef} playheadRef={playheadRef} + isSnappingToPlayhead={ + showSnapIndicator && currentSnapPoint?.type === "playhead" + } + /> + {/* Timeline Header with Ruler */}
@@ -1138,17 +1089,6 @@ export function Timeline() { ))} )} - - {/* Snap Indicator */} -
diff --git a/apps/web/src/hooks/use-timeline-snapping.ts b/apps/web/src/hooks/use-timeline-snapping.ts index c7e1f15d..f7f96392 100644 --- a/apps/web/src/hooks/use-timeline-snapping.ts +++ b/apps/web/src/hooks/use-timeline-snapping.ts @@ -1,10 +1,10 @@ import { useCallback } from "react"; -import { TimelineElement, TimelineTrack } from "@/types/timeline"; +import { TimelineTrack } from "@/types/timeline"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; export interface SnapPoint { time: number; - type: "grid" | "element-start" | "element-end" | "playhead"; + type: "element-start" | "element-end" | "playhead"; elementId?: string; trackId?: string; } @@ -17,16 +17,12 @@ export interface SnapResult { export interface UseTimelineSnappingOptions { snapThreshold?: number; // Distance in pixels to trigger snapping - gridInterval?: number; // Grid interval in seconds - enableGridSnapping?: boolean; enableElementSnapping?: boolean; enablePlayheadSnapping?: boolean; } export function useTimelineSnapping({ snapThreshold = 10, - gridInterval = 1, - enableGridSnapping = true, enableElementSnapping = true, enablePlayheadSnapping = true, }: UseTimelineSnappingOptions = {}) { @@ -40,23 +36,6 @@ export function useTimelineSnapping({ ): SnapPoint[] => { const snapPoints: SnapPoint[] = []; - // Add grid snap points - if (enableGridSnapping) { - const gridStart = Math.floor(currentTime / gridInterval) * gridInterval; - const gridEnd = Math.ceil(currentTime / gridInterval) * gridInterval; - - // Add nearby grid points - for (let i = -2; i <= 2; i++) { - const gridTime = gridStart + i * gridInterval; - if (gridTime >= 0) { - snapPoints.push({ - time: gridTime, - type: "grid", - }); - } - } - } - // Add element snap points if (enableElementSnapping) { tracks.forEach((track) => { @@ -97,12 +76,7 @@ export function useTimelineSnapping({ return snapPoints; }, - [ - enableGridSnapping, - enableElementSnapping, - enablePlayheadSnapping, - gridInterval, - ] + [enableElementSnapping, enablePlayheadSnapping] ); const snapToNearestPoint = useCallback( diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index 2734903a..f6b9bfd8 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -44,19 +44,9 @@ interface TimelineStore { // Snapping settings snappingEnabled: boolean; - gridSnappingEnabled: boolean; - elementSnappingEnabled: boolean; - playheadSnappingEnabled: boolean; - snapThreshold: number; - gridInterval: number; // Snapping actions toggleSnapping: () => void; - toggleGridSnapping: () => void; - toggleElementSnapping: () => void; - togglePlayheadSnapping: () => void; - setSnapThreshold: (threshold: number) => void; - setGridInterval: (interval: number) => void; // Multi-selection selectedElements: { trackId: string; elementId: string }[]; @@ -221,11 +211,6 @@ export const useTimelineStore = create((set, get) => { // Snapping settings defaults snappingEnabled: true, - gridSnappingEnabled: true, - elementSnappingEnabled: true, - playheadSnappingEnabled: true, - snapThreshold: 10, // pixels - gridInterval: 1, // seconds getSortedTracks: () => { const { _tracks } = get(); @@ -978,29 +963,5 @@ export const useTimelineStore = create((set, get) => { toggleSnapping: () => { set((state) => ({ snappingEnabled: !state.snappingEnabled })); }, - - toggleGridSnapping: () => { - set((state) => ({ gridSnappingEnabled: !state.gridSnappingEnabled })); - }, - - toggleElementSnapping: () => { - set((state) => ({ - elementSnappingEnabled: !state.elementSnappingEnabled, - })); - }, - - togglePlayheadSnapping: () => { - set((state) => ({ - playheadSnappingEnabled: !state.playheadSnappingEnabled, - })); - }, - - setSnapThreshold: (threshold) => { - set({ snapThreshold: Math.max(0, threshold) }); // Ensure non-negative threshold - }, - - setGridInterval: (interval) => { - set({ gridInterval: Math.max(0.1, interval) }); // Ensure minimum interval - }, }; }); -- cgit v1.3.1