diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 1ecbe54..584a45f 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1241,7 +1241,18 @@ function TimelineTrackContent({ }; const handleClipDragStart = (e: React.DragEvent, clip: any) => { - const dragData = { clipId: clip.id, trackId: track.id, name: clip.name }; + // Calculate the offset from the left edge of the clip to where the user clicked + const clipElement = e.currentTarget.parentElement as HTMLElement; + const clipRect = clipElement.getBoundingClientRect(); + const clickOffsetX = e.clientX - clipRect.left; + const clickOffsetTime = clickOffsetX / (50 * zoomLevel); + + const dragData = { + clipId: clip.id, + trackId: track.id, + name: clip.name, + clickOffsetTime: clickOffsetTime, + }; e.dataTransfer.setData( "application/x-timeline-clip", @@ -1467,7 +1478,11 @@ function TimelineTrackContent({ ); if (!timelineClipData) return; - const { clipId, trackId: fromTrackId } = JSON.parse(timelineClipData); + const { + clipId, + trackId: fromTrackId, + clickOffsetTime = 0, + } = JSON.parse(timelineClipData); // Find the clip being moved const sourceTrack = tracks.find( @@ -1480,10 +1495,17 @@ function TimelineTrackContent({ return; } + // Adjust position based on where user clicked on the clip + const adjustedStartTime = snappedTime - clickOffsetTime; + const finalStartTime = Math.max( + 0, + Math.round(adjustedStartTime * 10) / 10 + ); + // Check for overlaps with existing clips (excluding the moving clip itself) const movingClipDuration = movingClip.duration - movingClip.trimStart - movingClip.trimEnd; - const movingClipEnd = snappedTime + movingClipDuration; + const movingClipEnd = finalStartTime + movingClipDuration; const hasOverlap = track.clips.some((existingClip) => { // Skip the clip being moved if it's on the same track @@ -1498,7 +1520,7 @@ function TimelineTrackContent({ existingClip.trimEnd); // Check if clips overlap - return snappedTime < existingEnd && movingClipEnd > existingStart; + return finalStartTime < existingEnd && movingClipEnd > existingStart; }); if (hasOverlap) { @@ -1510,12 +1532,12 @@ function TimelineTrackContent({ if (fromTrackId === track.id) { // Moving within same track - updateClipStartTime(track.id, clipId, snappedTime); + updateClipStartTime(track.id, clipId, finalStartTime); } else { // Moving to different track moveClipToTrack(fromTrackId, track.id, clipId); requestAnimationFrame(() => { - updateClipStartTime(track.id, clipId, snappedTime); + updateClipStartTime(track.id, clipId, finalStartTime); }); } } else if (hasMediaItem) { @@ -1612,6 +1634,7 @@ function TimelineTrackContent({ src={mediaItem.url} alt={mediaItem.name} className="w-full h-full object-cover" + draggable={false} /> ); @@ -1625,6 +1648,7 @@ function TimelineTrackContent({ src={mediaItem.thumbnailUrl} alt={mediaItem.name} className="w-full h-full object-cover rounded-sm" + draggable={false} /> @@ -1684,13 +1708,7 @@ function TimelineTrackContent({ return (
{ e.preventDefault(); // Only show track menu if we didn't click on a clip @@ -1744,7 +1762,7 @@ function TimelineTrackContent({ return (
{ e.stopPropagation(); @@ -1835,30 +1853,6 @@ function TimelineTrackContent({
); })} - - {/* Drop position indicator */} - {isDraggedOver && dropPosition !== null && ( -
-
-
-
- {wouldOverlap ? "⚠️" : ""} - {dropPosition.toFixed(1)}s -
-
- )} )}