diff options
Diffstat (limited to 'apps/web/src/components/editor')
| -rw-r--r-- | apps/web/src/components/editor/media-panel/views/media.tsx | 58 | ||||
| -rw-r--r-- | apps/web/src/components/editor/media-panel/views/text.tsx | 37 | ||||
| -rw-r--r-- | apps/web/src/components/editor/snap-indicator.tsx | 54 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline-playhead.tsx | 14 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline-track.tsx | 50 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline.tsx | 383 |
6 files changed, 371 insertions, 225 deletions
diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx index 48e54961..393d72db 100644 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ b/apps/web/src/components/editor/media-panel/views/media.tsx @@ -24,6 +24,7 @@ import { } from "@/components/ui/select";
import { DraggableMediaItem } from "@/components/ui/draggable-item";
import { useProjectStore } from "@/stores/project-store";
+import { useTimelineStore } from "@/stores/timeline-store";
export function MediaView() {
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
@@ -207,9 +208,8 @@ export function MediaView() { <DragOverlay isVisible={isDragOver} />
<div className="p-3 pb-2">
- {/* Button to add/upload media */}
+ {/* Search and filter controls */}
<div className="flex gap-2">
- {/* Search and filter controls */}
<Select value={mediaFilter} onValueChange={setMediaFilter}>
<SelectTrigger className="w-[80px] h-full text-xs">
<SelectValue />
@@ -228,29 +228,6 @@ export function MediaView() { value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
-
- {/* Add media button */}
- <Button
- variant="outline"
- size="sm"
- onClick={handleFileSelect}
- disabled={isProcessing}
- className="flex-none bg-transparent min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center"
- >
- {isProcessing ? (
- <>
- <Upload className="h-4 w-4 animate-spin" />
- <span className="hidden md:inline ml-2">{progress}%</span>
- </>
- ) : (
- <>
- <Plus className="h-4 w-4" />
- <span className="hidden sm:inline ml-2" aria-label="Add file">
- Add
- </span>
- </>
- )}
- </Button>
</div>
</div>
@@ -265,8 +242,32 @@ export function MediaView() { No media in project
</p>
<p className="text-xs text-muted-foreground/70 mt-1">
- Drag files here or use the button above
+ Drag files here or use the button below
</p>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleFileSelect}
+ disabled={isProcessing}
+ className="flex-none bg-transparent min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center mt-2"
+ >
+ {isProcessing ? (
+ <>
+ <Upload className="h-4 w-4 animate-spin" />
+ <span className="hidden md:inline ml-2">{progress}%</span>
+ </>
+ ) : (
+ <>
+ <Plus className="h-4 w-4" />
+ <span
+ className="hidden sm:inline ml-2"
+ aria-label="Add file"
+ >
+ Add
+ </span>
+ </>
+ )}
+ </Button>
</div>
) : (
<div
@@ -288,6 +289,11 @@ export function MediaView() { name: item.name,
}}
showPlusOnDrag={false}
+ onAddToTimeline={(currentTime) =>
+ useTimelineStore
+ .getState()
+ .addMediaAtTime(item, currentTime)
+ }
rounded={false}
/>
</ContextMenuTrigger>
diff --git a/apps/web/src/components/editor/media-panel/views/text.tsx b/apps/web/src/components/editor/media-panel/views/text.tsx index bec32c85..6d0d310b 100644 --- a/apps/web/src/components/editor/media-panel/views/text.tsx +++ b/apps/web/src/components/editor/media-panel/views/text.tsx @@ -1,4 +1,30 @@ import { DraggableMediaItem } from "@/components/ui/draggable-item";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { useTimelineStore } from "@/stores/timeline-store";
+import { type TextElement } from "@/types/timeline";
+
+let textData: TextElement = {
+ id: "default-text",
+ type: "text",
+ name: "Default text",
+ content: "Default text",
+ fontSize: 48,
+ fontFamily: "Arial",
+ color: "#ffffff",
+ backgroundColor: "transparent",
+ textAlign: "center" as const,
+ fontWeight: "normal" as const,
+ fontStyle: "normal" as const,
+ textDecoration: "none" as const,
+ x: 0,
+ y: 0,
+ rotation: 0,
+ opacity: 1,
+ duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+};
export function TextView() {
return (
@@ -11,12 +37,15 @@ export function TextView() { </div>
}
dragData={{
- id: "default-text",
- type: "text",
- name: "Default text",
- content: "Default text",
+ id: textData.id,
+ type: textData.type,
+ name: textData.name,
+ content: textData.content,
}}
aspectRatio={1}
+ onAddToTimeline={(currentTime) =>
+ useTimelineStore.getState().addTextAtTime(textData, currentTime)
+ }
showLabel={false}
/>
</div>
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..83a1f678 --- /dev/null +++ b/apps/web/src/components/editor/snap-indicator.tsx @@ -0,0 +1,54 @@ +"use client"; + +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; + isVisible: boolean; + tracks: TimelineTrack[]; + timelineRef: React.RefObject<HTMLDivElement>; + trackLabelsRef?: React.RefObject<HTMLDivElement>; +} + +export function SnapIndicator({ + snapPoint, + zoomLevel, + isVisible, + tracks, + timelineRef, + trackLabelsRef, +}: SnapIndicatorProps) { + if (!isVisible || !snapPoint) { + return null; + } + + const timelineContainerHeight = timelineRef.current?.offsetHeight || 400; + const totalHeight = timelineContainerHeight - 8; // 8px padding from edges + + // 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 leftPosition = + trackLabelsWidth + + snapPoint.time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; + + return ( + <div + className="absolute pointer-events-none z-[90]" + style={{ + left: `${leftPosition}px`, + top: 0, + height: `${totalHeight}px`, + width: "2px", + }} + > + <div className={`w-0.5 h-full bg-primary/40 opacity-80`} /> + </div> + ); +} 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<HTMLDivElement>;
timelineRef: React.RefObject<HTMLDivElement>;
playheadRef?: React.RefObject<HTMLDivElement>;
+ isSnappingToPlayhead?: boolean;
}
export function TimelinePlayhead({
@@ -34,6 +35,7 @@ export function TimelinePlayhead({ trackLabelsRef,
timelineRef,
playheadRef: externalPlayheadRef,
+ isSnappingToPlayhead = false,
}: TimelinePlayheadProps) {
const internalPlayheadRef = useRef<HTMLDivElement>(null);
const playheadRef = externalPlayheadRef || internalPlayheadRef;
@@ -73,11 +75,15 @@ export function TimelinePlayhead({ }}
onMouseDown={handlePlayheadMouseDown}
>
- {/* The red line spanning full height */}
- <div className="absolute left-0 w-0.5 bg-foreground cursor-col-resize h-full" />
+ {/* The playhead line spanning full height */}
+ <div
+ className={`absolute left-0 w-0.5 cursor-col-resize h-full ${isSnappingToPlayhead ? "bg-primary" : "bg-foreground"}`}
+ />
- {/* Red dot indicator at the top (in ruler area) */}
- <div className="absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 bg-foreground rounded-full border-2 border-foreground shadow-sm" />
+ {/* Playhead dot indicator at the top (in ruler area) */}
+ <div
+ className={`absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 rounded-full border-2 shadow-sm ${isSnappingToPlayhead ? "bg-primary border-primary" : "bg-foreground border-foreground"}`}
+ />
</div>
);
}
diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index 033b79a6..2be143eb 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -22,13 +22,16 @@ import { TIMELINE_CONSTANTS, } from "@/constants/timeline-constants"; import { useProjectStore } from "@/stores/project-store"; +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 { @@ -45,8 +48,18 @@ export function TimelineTrackContent({ endDrag: endDragAction, clearSelectedElements, insertTrackAt, + snappingEnabled, } = useTimelineStore(); + const { currentTime } = usePlaybackStore(); + + // Initialize snapping hook + const { snapElementPosition } = useTimelineSnapping({ + snapThreshold: 10, + enableElementSnapping: snappingEnabled, + enablePlayheadSnapping: snappingEnabled, + }); + const timelineRef = useRef<HTMLDivElement>(null); const [isDropping, setIsDropping] = useState(false); const [dropPosition, setDropPosition] = useState<number | null>(null); @@ -85,12 +98,34 @@ 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; + let snapPoint = null; + if (snappingEnabled) { + const snapResult = snapElementPosition( + adjustedTime, + tracks, + currentTime, + zoomLevel, + 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); }; const handleMouseUp = (e: MouseEvent) => { @@ -108,6 +143,8 @@ export function TimelineTrackContent({ dragState.currentTime ); endDragAction(); + // Clear snap point when drag ends + onSnapPointChange?.(null); } return; } @@ -204,6 +241,8 @@ export function TimelineTrackContent({ if (isTrackThatStartedDrag) { endDragAction(); + // Clear snap point when drag ends + onSnapPointChange?.(null); } }; @@ -229,6 +268,7 @@ export function TimelineTrackContent({ endDragAction, selectedElements, selectElement, + onSnapPointChange, ]); const handleElementMouseDown = ( diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 71e2997b..c1603542 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -15,6 +15,8 @@ import { Video, Music, TypeIcon, + Magnet, + Lock, } from "lucide-react"; import { Tooltip, @@ -36,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, @@ -50,6 +45,8 @@ import { } from "./timeline-playhead"; import { SelectionBox } from "./selection-box"; import { useSelectionBox } from "@/hooks/use-selection-box"; +import { SnapIndicator } from "./snap-indicator"; +import { SnapPoint } from "@/hooks/use-timeline-snapping"; import type { DragData, TimelineTrack } from "@/types/timeline"; import { getTrackHeight, @@ -78,6 +75,9 @@ export function Timeline() { separateAudio, undo, redo, + snappingEnabled, + toggleSnapping, + dragState, } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { activeProject } = useProjectStore(); @@ -118,7 +118,7 @@ export function Timeline() { const lastVerticalSync = useRef(0); // Timeline playhead ruler handlers - const { handleRulerMouseDown, isDraggingRuler } = useTimelinePlayheadRuler({ + const { handleRulerMouseDown } = useTimelinePlayheadRuler({ currentTime, duration, zoomLevel, @@ -145,6 +145,18 @@ export function Timeline() { }, }); + // Calculate snap indicator state + const [currentSnapPoint, setCurrentSnapPoint] = useState<SnapPoint | null>( + null + ); + const showSnapIndicator = + 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( (e: React.MouseEvent) => { @@ -364,29 +376,7 @@ export function Timeline() { if (dragData.type === "text") { // Always create new text track to avoid overlaps - const newTrackId = addTrack("text"); - - addElementToTrack(newTrackId, { - type: "text", - name: dragData.name || "Text", - content: dragData.content || "Default Text", - duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, - startTime: 0, - trimStart: 0, - trimEnd: 0, - fontSize: 48, - fontFamily: "Arial", - color: "#ffffff", - backgroundColor: "transparent", - textAlign: "center", - fontWeight: "normal", - fontStyle: "normal", - textDecoration: "none", - x: 0, - y: 0, - rotation: 0, - opacity: 1, - }); + useTimelineStore.getState().addTextToNewTrack(dragData); } else { // Handle media items const mediaItem = mediaItems.find((item) => item.id === dragData.id); @@ -395,19 +385,7 @@ export function Timeline() { return; } - const trackType = dragData.type === "audio" ? "audio" : "media"; - let targetTrack = tracks.find((t) => t.type === trackType); - const newTrackId = targetTrack ? targetTrack.id : addTrack(trackType); - - addElementToTrack(newTrackId, { - type: "media", - mediaId: mediaItem.id, - name: mediaItem.name, - duration: mediaItem.duration || 5, - startTime: 0, - trimStart: 0, - trimEnd: 0, - }); + useTimelineStore.getState().addMediaToNewTrack(mediaItem); } } catch (error) { console.error("Error parsing dropped item data:", error); @@ -435,18 +413,7 @@ export function Timeline() { item.name === processedItem.name && item.url === processedItem.url ); if (addedItem) { - const trackType = - processedItem.type === "audio" ? "audio" : "media"; - const newTrackId = addTrack(trackType); - addElementToTrack(newTrackId, { - type: "media", - mediaId: addedItem.id, - name: addedItem.name, - duration: addedItem.duration || 5, - startTime: 0, - trimStart: 0, - trimEnd: 0, - }); + useTimelineStore.getState().addMediaToNewTrack(addedItem); } } } catch (error) { @@ -686,136 +653,168 @@ export function Timeline() { onMouseLeave={() => setIsInTimeline(false)} > {/* Toolbar */} - <div className="border-b flex items-center px-2 py-1 gap-1"> - <TooltipProvider delayDuration={500}> - {/* Play/Pause Button */} - <Tooltip> - <TooltipTrigger asChild> - <Button - variant="text" - size="icon" - onClick={toggle} - className="mr-2" - > - {isPlaying ? ( - <Pause className="h-4 w-4" /> - ) : ( - <Play className="h-4 w-4" /> - )} - </Button> - </TooltipTrigger> - <TooltipContent> - {isPlaying ? "Pause (Space)" : "Play (Space)"} - </TooltipContent> - </Tooltip> - <div className="w-px h-6 bg-border mx-1" /> - {/* Time Display */} - <div - className="text-xs text-muted-foreground font-mono px-2" - style={{ minWidth: "18ch", textAlign: "center" }} - > - {currentTime.toFixed(1)}s / {duration.toFixed(1)}s - </div> - {/* Test Clip Button - for debugging */} - {tracks.length === 0 && ( - <> - <div className="w-px h-6 bg-border mx-1" /> - <Tooltip> - <TooltipTrigger asChild> - <Button - variant="outline" - size="sm" - onClick={() => { - const trackId = addTrack("media"); - addElementToTrack(trackId, { - type: "media", - mediaId: "test", - name: "Test Clip", - duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, - startTime: 0, - trimStart: 0, - trimEnd: 0, - }); - }} - className="text-xs" - > - Add Test Clip - </Button> - </TooltipTrigger> - <TooltipContent>Add a test clip to try playback</TooltipContent> - </Tooltip> - </> - )} - <div className="w-px h-6 bg-border mx-1" /> - <Tooltip> - <TooltipTrigger asChild> - <Button variant="text" size="icon" onClick={handleSplitSelected}> - <Scissors className="h-4 w-4" /> - </Button> - </TooltipTrigger> - <TooltipContent>Split element (Ctrl+S)</TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - variant="text" - size="icon" - onClick={handleSplitAndKeepLeft} - > - <ArrowLeftToLine className="h-4 w-4" /> - </Button> - </TooltipTrigger> - <TooltipContent>Split and keep left (Ctrl+Q)</TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - variant="text" - size="icon" - onClick={handleSplitAndKeepRight} - > - <ArrowRightToLine className="h-4 w-4" /> - </Button> - </TooltipTrigger> - <TooltipContent>Split and keep right (Ctrl+W)</TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button variant="text" size="icon" onClick={handleSeparateAudio}> - <SplitSquareHorizontal className="h-4 w-4" /> - </Button> - </TooltipTrigger> - <TooltipContent>Separate audio (Ctrl+D)</TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - variant="text" - size="icon" - onClick={handleDuplicateSelected} - > - <Copy className="h-4 w-4" /> - </Button> - </TooltipTrigger> - <TooltipContent>Duplicate element (Ctrl+D)</TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button variant="text" size="icon" onClick={handleFreezeSelected}> - <Snowflake className="h-4 w-4" /> - </Button> - </TooltipTrigger> - <TooltipContent>Freeze frame (F)</TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button variant="text" size="icon" onClick={handleDeleteSelected}> - <Trash2 className="h-4 w-4" /> - </Button> - </TooltipTrigger> - <TooltipContent>Delete element (Delete)</TooltipContent> - </Tooltip> - </TooltipProvider> + <div className="border-b flex items-center justify-between px-2 py-1"> + <div className="flex items-center gap-1 w-full"> + <TooltipProvider delayDuration={500}> + {/* Play/Pause Button */} + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="text" + size="icon" + onClick={toggle} + className="mr-2" + > + {isPlaying ? ( + <Pause className="h-4 w-4" /> + ) : ( + <Play className="h-4 w-4" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent> + {isPlaying ? "Pause (Space)" : "Play (Space)"} + </TooltipContent> + </Tooltip> + <div className="w-px h-6 bg-border mx-1" /> + {/* Time Display */} + <div + className="text-xs text-muted-foreground font-mono px-2" + style={{ minWidth: "18ch", textAlign: "center" }} + > + {currentTime.toFixed(1)}s / {duration.toFixed(1)}s + </div> + {/* Test Clip Button - for debugging */} + {tracks.length === 0 && ( + <> + <div className="w-px h-6 bg-border mx-1" /> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + onClick={() => { + const trackId = addTrack("media"); + addElementToTrack(trackId, { + type: "media", + mediaId: "test", + name: "Test Clip", + duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, + startTime: 0, + trimStart: 0, + trimEnd: 0, + }); + }} + className="text-xs" + > + Add Test Clip + </Button> + </TooltipTrigger> + <TooltipContent> + Add a test clip to try playback + </TooltipContent> + </Tooltip> + </> + )} + <div className="w-px h-6 bg-border mx-1" /> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="text" + size="icon" + onClick={handleSplitSelected} + > + <Scissors className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Split element (Ctrl+S)</TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="text" + size="icon" + onClick={handleSplitAndKeepLeft} + > + <ArrowLeftToLine className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Split and keep left (Ctrl+Q)</TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="text" + size="icon" + onClick={handleSplitAndKeepRight} + > + <ArrowRightToLine className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Split and keep right (Ctrl+W)</TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="text" + size="icon" + onClick={handleSeparateAudio} + > + <SplitSquareHorizontal className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Separate audio (Ctrl+D)</TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="text" + size="icon" + onClick={handleDuplicateSelected} + > + <Copy className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Duplicate element (Ctrl+D)</TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="text" + size="icon" + onClick={handleFreezeSelected} + > + <Snowflake className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Freeze frame (F)</TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="text" + size="icon" + onClick={handleDeleteSelected} + > + <Trash2 className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Delete element (Delete)</TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + <div className="flex items-center gap-1"> + <TooltipProvider delayDuration={500}> + <Tooltip> + <TooltipTrigger asChild> + <Button variant="text" size="icon" onClick={toggleSnapping}> + <Lock className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent>Auto snapping</TooltipContent> + </Tooltip> + </TooltipProvider> + </div> </div> {/* Timeline Container */} @@ -835,6 +834,17 @@ export function Timeline() { trackLabelsRef={trackLabelsRef} timelineRef={timelineRef} playheadRef={playheadRef} + isSnappingToPlayhead={ + showSnapIndicator && currentSnapPoint?.type === "playhead" + } + /> + <SnapIndicator + snapPoint={currentSnapPoint} + zoomLevel={zoomLevel} + tracks={tracks} + timelineRef={timelineRef} + trackLabelsRef={trackLabelsRef} + isVisible={showSnapIndicator} /> {/* Timeline Header with Ruler */} <div className="flex bg-panel sticky top-0 z-10"> @@ -1016,6 +1026,7 @@ export function Timeline() { <TimelineTrackContent track={track} zoomLevel={zoomLevel} + onSnapPointChange={handleSnapPointChange} /> </div> </ContextMenuTrigger> |
