diff options
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/components/editor/timeline.tsx | 136 |
1 files changed, 136 insertions, 0 deletions
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<any>(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() { </TooltipTrigger> <TooltipContent>Delete element (Delete)</TooltipContent> </Tooltip> + + <div className="w-px h-6 bg-border mx-1" /> + + {/* Snapping Controls */} + <Tooltip> + <TooltipTrigger asChild> + <Button + variant={snappingEnabled ? "default" : "text"} + size="icon" + onClick={toggleSnapping} + > + <Magnet className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Toggle snapping (S)</TooltipContent> + </Tooltip> + + <Tooltip> + <TooltipTrigger asChild> + <Button + variant={ + gridSnappingEnabled && snappingEnabled ? "default" : "text" + } + size="icon" + onClick={toggleGridSnapping} + disabled={!snappingEnabled} + > + <Grid3X3 className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Toggle grid snapping</TooltipContent> + </Tooltip> + + <Tooltip> + <TooltipTrigger asChild> + <Button + variant={ + elementSnappingEnabled && snappingEnabled ? "default" : "text" + } + size="icon" + onClick={toggleElementSnapping} + disabled={!snappingEnabled} + > + <Move className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Toggle element snapping</TooltipContent> + </Tooltip> + + <Tooltip> + <TooltipTrigger asChild> + <Button + variant={ + playheadSnappingEnabled && snappingEnabled + ? "default" + : "text" + } + size="icon" + onClick={togglePlayheadSnapping} + disabled={!snappingEnabled} + > + <Target className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Toggle playhead snapping</TooltipContent> + </Tooltip> </TooltipProvider> </div> @@ -1033,6 +1158,17 @@ export function Timeline() { ))} </> )} + + {/* Snap Indicator */} + <SnapIndicator + snapPoint={currentSnapPoint} + zoomLevel={zoomLevel} + timelineHeight={Math.max( + 200, + Math.min(800, getTotalTracksHeight(tracks)) + )} + isVisible={showSnapIndicator} + /> </div> </ScrollArea> </div> |
