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 */} +