feat: add snapping functionality and controls to timeline component

This commit is contained in:
Anwarul Islam 2025-07-15 04:27:12 +06:00
parent e7485340cf
commit af632d8cc5
1 changed files with 136 additions and 0 deletions

View File

@ -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>