diff --git a/apps/web/src/components/editor/timeline/timeline-track.tsx b/apps/web/src/components/editor/timeline/timeline-track.tsx index 097fc051..1982d03e 100644 --- a/apps/web/src/components/editor/timeline/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline/timeline-track.tsx @@ -68,45 +68,51 @@ export function TimelineTrackContent({ elementDuration: number, excludeElementId?: string ) => { - if (!snappingEnabled) { - // Use frame snapping if project has FPS, otherwise use decimal snapping - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - return snapTimeToFrame(dropTime, projectFps); + // Always apply frame snapping first + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + let finalTime = snapTimeToFrame(dropTime, projectFps); + + // Additionally apply element snapping if enabled + if (snappingEnabled) { + // Try snapping both start and end edges for drops + const startSnapResult = snapElementEdge( + dropTime, + elementDuration, + tracks, + currentTime, + zoomLevel, + excludeElementId, + true // snap to start edge + ); + + const endSnapResult = snapElementEdge( + dropTime, + elementDuration, + tracks, + currentTime, + zoomLevel, + excludeElementId, + false // snap to end edge + ); + + // Choose the snap result with the smaller distance (closer snap) + let bestSnapResult = startSnapResult; + if ( + endSnapResult.snapPoint && + (!startSnapResult.snapPoint || + endSnapResult.snapDistance < startSnapResult.snapDistance) + ) { + bestSnapResult = endSnapResult; + } + + // Only use element snapping if it found a snap point, otherwise keep frame-snapped time + if (bestSnapResult.snapPoint) { + finalTime = bestSnapResult.snappedTime; + } } - // Try snapping both start and end edges for drops - const startSnapResult = snapElementEdge( - dropTime, - elementDuration, - tracks, - currentTime, - zoomLevel, - excludeElementId, - true // snap to start edge - ); - - const endSnapResult = snapElementEdge( - dropTime, - elementDuration, - tracks, - currentTime, - zoomLevel, - excludeElementId, - false // snap to end edge - ); - - // Choose the snap result with the smaller distance (closer snap) - let bestSnapResult = startSnapResult; - if ( - endSnapResult.snapPoint && - (!startSnapResult.snapPoint || - endSnapResult.snapDistance < startSnapResult.snapDistance) - ) { - bestSnapResult = endSnapResult; - } - - return bestSnapResult.snappedTime; + return finalTime; }; const timelineRef = useRef(null); @@ -148,9 +154,13 @@ export function TimelineTrackContent({ ); const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime); - // Apply snapping if enabled - let finalTime = adjustedTime; + // Always apply frame snapping first + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + let finalTime = snapTimeToFrame(adjustedTime, projectFps); let snapPoint = null; + + // Additionally apply element snapping if enabled if (snappingEnabled) { // Find the element being dragged to get its duration let elementDuration = 5; // fallback duration @@ -196,18 +206,16 @@ export function TimelineTrackContent({ bestSnapResult = endSnapResult; } - finalTime = bestSnapResult.snappedTime; - snapPoint = bestSnapResult.snapPoint; + // Only use element snapping if it found a snap point, otherwise keep frame-snapped time + if (bestSnapResult.snapPoint) { + finalTime = bestSnapResult.snappedTime; + snapPoint = bestSnapResult.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 + // Clear snap point when element snapping is disabled onSnapPointChange?.(null); } diff --git a/apps/web/src/hooks/use-timeline-element-resize.ts b/apps/web/src/hooks/use-timeline-element-resize.ts index d57533e0..ba13e05a 100644 --- a/apps/web/src/hooks/use-timeline-element-resize.ts +++ b/apps/web/src/hooks/use-timeline-element-resize.ts @@ -221,17 +221,34 @@ export function useTimelineElementResize({ } } else { // Normal trimming within original duration - const maxTrimEnd = element.duration - resizing.initialTrimStart - 0.1; // Leave at least 0.1s visible - const newTrimEnd = snapTimeToFrame( - Math.max(0, Math.min(maxTrimEnd, calculated)), - projectFps + // Calculate the desired end time based on mouse movement + const currentEndTime = + element.startTime + + element.duration - + element.trimStart - + element.trimEnd; + const desiredEndTime = currentEndTime + deltaTime; + + // Snap the desired end time to frame + const snappedEndTime = snapTimeToFrame(desiredEndTime, projectFps); + + // Calculate what trimEnd should be to achieve this snapped end time + const newTrimEnd = Math.max( + 0, + element.duration - + element.trimStart - + (snappedEndTime - element.startTime) ); + // Ensure we don't trim more than available content (leave at least 0.1s visible) + const maxTrimEnd = element.duration - element.trimStart - 0.1; + const finalTrimEnd = Math.min(maxTrimEnd, newTrimEnd); + updateElementTrim( track.id, element.id, - resizing.initialTrimStart, - newTrimEnd, + element.trimStart, + finalTrimEnd, false ); }