@@ -122,6 +122,7 @@ export function TimelineTrackContent({
122122 } ;
123123
124124 const timelineRef = useRef < HTMLDivElement > ( null ) ;
125+ const trackContainerRef = useRef < HTMLDivElement > ( null ) ;
125126 const [ isDropping , setIsDropping ] = useState ( false ) ;
126127 const [ dropPosition , setDropPosition ] = useState < number | null > ( null ) ;
127128 const [ wouldOverlap , setWouldOverlap ] = useState ( false ) ;
@@ -132,6 +133,19 @@ export function TimelineTrackContent({
132133 } | null > ( null ) ;
133134
134135 const lastMouseXRef = useRef ( 0 ) ;
136+ const [ dragOverlay , setDragOverlay ] = useState < {
137+ show : boolean ;
138+ time : number ;
139+ duration : number ;
140+ trackId : string | null ;
141+ } | null > ( null ) ;
142+
143+ // Clear drag overlay when drag ends
144+ useEffect ( ( ) => {
145+ if ( ! dragState . isDragging ) {
146+ setDragOverlay ( null ) ;
147+ }
148+ } , [ dragState . isDragging ] ) ;
135149
136150 // Set up mouse event listeners for drag
137151 useEffect ( ( ) => {
@@ -229,16 +243,126 @@ export function TimelineTrackContent({
229243 }
230244
231245 updateDragTime ( finalTime ) ;
246+
247+ // Update drag overlay to show where element will be dropped
248+ // Show overlay on the track the mouse is currently over
249+ if ( dragState . elementId && dragState . trackId ) {
250+ const sourceTrack = tracks . find ( ( t ) => t . id === dragState . trackId ) ;
251+ const element = sourceTrack ?. elements . find (
252+ ( e ) => e . id === dragState . elementId
253+ ) ;
254+ if ( element ) {
255+ const elementDuration =
256+ element . duration - element . trimStart - element . trimEnd ;
257+
258+ // Check if mouse is over this track
259+ const trackContainer = trackContainerRef . current ;
260+ const trackContainerRect = trackContainer ?. getBoundingClientRect ( ) ;
261+
262+ const isMouseOverThisTrack =
263+ trackContainerRect &&
264+ e . clientY >= trackContainerRect . top &&
265+ e . clientY <= trackContainerRect . bottom ;
266+
267+ if ( isMouseOverThisTrack ) {
268+ // Only show overlay for tracks above the source track
269+ const sourceTrackIndex = tracks . findIndex (
270+ ( t ) => t . id === dragState . trackId
271+ ) ;
272+ const currentTrackIndex = tracks . findIndex (
273+ ( t ) => t . id === track . id
274+ ) ;
275+
276+ // Only show overlay if current track is above the source track
277+ if ( currentTrackIndex < sourceTrackIndex ) {
278+ // Calculate overlay time using same logic as drop handler
279+ let overlayTime = finalTime ;
280+ const tracksContainer = tracksScrollRef . current ;
281+
282+ if ( tracksContainer ) {
283+ const containerRect = tracksContainer . getBoundingClientRect ( ) ;
284+ const scrollLeft = tracksContainer . scrollLeft ;
285+ const mouseX = e . clientX - containerRect . left ;
286+ const mouseTime = Math . max (
287+ 0 ,
288+ ( mouseX + scrollLeft ) /
289+ ( TIMELINE_CONSTANTS . PIXELS_PER_SECOND * zoomLevel )
290+ ) ;
291+ const adjustedTime = Math . max (
292+ 0 ,
293+ mouseTime - dragState . clickOffsetTime
294+ ) ;
295+ const projectStore = useProjectStore . getState ( ) ;
296+ const projectFps =
297+ projectStore . activeProject ?. fps || DEFAULT_FPS ;
298+ overlayTime = snapTimeToFrame ( adjustedTime , projectFps ) ;
299+ }
300+
301+ setDragOverlay ( {
302+ show : true ,
303+ time : overlayTime ,
304+ duration : elementDuration ,
305+ trackId : track . id ,
306+ } ) ;
307+ } else {
308+ // Clear overlay if not above source track
309+ setDragOverlay ( ( prev ) =>
310+ prev ?. trackId === track . id ? null : prev
311+ ) ;
312+ }
313+ } else {
314+ // Clear overlay if mouse left this track
315+ setDragOverlay ( ( prev ) =>
316+ prev ?. trackId === track . id ? null : prev
317+ ) ;
318+ }
319+ }
320+ }
232321 } ;
233322
234323 const handleMouseUp = ( e : MouseEvent ) => {
235- if ( ! dragState . elementId || ! dragState . trackId ) return ;
324+ if ( ! dragState . elementId || ! dragState . trackId || ! dragState . isDragging ) {
325+ return ;
326+ }
236327
237- // If this track initiated the drag, we should handle the mouse up regardless of where it occurs
328+ // First, check if we should handle this mouseup at all for this track
238329 const isTrackThatStartedDrag = dragState . trackId === track . id ;
239330
331+ // Get all necessary rects upfront
332+ const trackContainer = trackContainerRef . current ;
333+ const trackContainerRect = trackContainer ?. getBoundingClientRect ( ) ;
240334 const timelineRect = timelineRef . current ?. getBoundingClientRect ( ) ;
241- if ( ! timelineRect ) {
335+
336+ // Check if mouse is over this track
337+ let isMouseOverThisTrack = false ;
338+
339+ if ( trackContainerRect ) {
340+ const mouseX = e . clientX ;
341+ const mouseY = e . clientY ;
342+
343+ // Check if directly over the track
344+ const isDirectlyOver =
345+ mouseY >= trackContainerRect . top &&
346+ mouseY <= trackContainerRect . bottom &&
347+ mouseX >= trackContainerRect . left &&
348+ mouseX <= trackContainerRect . right ;
349+
350+ if ( isDirectlyOver ) {
351+ isMouseOverThisTrack = true ;
352+ }
353+ } else if ( timelineRect ) {
354+ // Fallback to timeline rect if track container not available
355+ isMouseOverThisTrack =
356+ e . clientY >= timelineRect . top &&
357+ e . clientY <= timelineRect . bottom &&
358+ e . clientX >= timelineRect . left &&
359+ e . clientX <= timelineRect . right ;
360+ }
361+
362+ const tracksContainer = tracksScrollRef . current ;
363+
364+ // If we don't have track container bounds, try to handle with source track only
365+ if ( ! trackContainerRect ) {
242366 if ( isTrackThatStartedDrag ) {
243367 if ( rippleEditingEnabled ) {
244368 updateElementStartTimeWithRipple (
@@ -260,14 +384,44 @@ export function TimelineTrackContent({
260384 return ;
261385 }
262386
263- const isMouseOverThisTrack =
264- e . clientY >= timelineRect . top && e . clientY <= timelineRect . bottom ;
265-
266- if ( ! isMouseOverThisTrack && ! isTrackThatStartedDrag ) return ;
387+ if ( ! isMouseOverThisTrack && ! isTrackThatStartedDrag ) {
388+ // Clear overlay if mouse left this track
389+ setDragOverlay ( ( prev ) => ( prev ?. trackId === track . id ? null : prev ) ) ;
390+ return ;
391+ }
267392
268- const finalTime = dragState . currentTime ;
393+ // Calculate final time - always use tracksScrollRef for consistent calculation
394+ // This works for both empty and non-empty tracks
395+ let finalTime = dragState . currentTime ;
396+ if ( tracksContainer ) {
397+ const containerRect = tracksContainer . getBoundingClientRect ( ) ;
398+ const scrollLeft = tracksContainer . scrollLeft ;
399+ const mouseX = e . clientX - containerRect . left ;
400+ const mouseTime = Math . max (
401+ 0 ,
402+ ( mouseX + scrollLeft ) /
403+ ( TIMELINE_CONSTANTS . PIXELS_PER_SECOND * zoomLevel )
404+ ) ;
405+ const adjustedTime = Math . max ( 0 , mouseTime - dragState . clickOffsetTime ) ;
406+ const projectStore = useProjectStore . getState ( ) ;
407+ const projectFps = projectStore . activeProject ?. fps || DEFAULT_FPS ;
408+ finalTime = snapTimeToFrame ( adjustedTime , projectFps ) ;
409+ } else if ( timelineRect ) {
410+ // Fallback to timeline rect if scroll ref not available
411+ const mouseX = e . clientX - timelineRect . left ;
412+ const mouseTime = Math . max (
413+ 0 ,
414+ mouseX / ( TIMELINE_CONSTANTS . PIXELS_PER_SECOND * zoomLevel )
415+ ) ;
416+ const adjustedTime = Math . max ( 0 , mouseTime - dragState . clickOffsetTime ) ;
417+ const projectStore = useProjectStore . getState ( ) ;
418+ const projectFps = projectStore . activeProject ?. fps || DEFAULT_FPS ;
419+ finalTime = snapTimeToFrame ( adjustedTime , projectFps ) ;
420+ }
269421
422+ // Handle drop on track
270423 if ( isMouseOverThisTrack ) {
424+ // PRIORITY 2: Handle drop ON track
271425 const sourceTrack = tracks . find ( ( t ) => t . id === dragState . trackId ) ;
272426 const movingElement = sourceTrack ?. elements . find (
273427 ( c ) => c . id === dragState . elementId
@@ -281,21 +435,28 @@ export function TimelineTrackContent({
281435 const movingElementEnd = finalTime + movingElementDuration ;
282436
283437 const targetTrack = tracks . find ( ( t ) => t . id === track . id ) ;
284- const hasOverlap = targetTrack ?. elements . some ( ( existingElement ) => {
285- if (
286- dragState . trackId === track . id &&
287- existingElement . id === dragState . elementId
288- ) {
289- return false ;
290- }
291- const existingStart = existingElement . startTime ;
292- const existingEnd =
293- existingElement . startTime +
294- ( existingElement . duration -
295- existingElement . trimStart -
296- existingElement . trimEnd ) ;
297- return finalTime < existingEnd && movingElementEnd > existingStart ;
298- } ) ;
438+ // For empty tracks, elements array is empty, so some() returns false (no overlap)
439+ // This is correct - empty tracks can always accept drops
440+ const hasOverlap = targetTrack
441+ ? targetTrack . elements . some ( ( existingElement ) => {
442+ // Skip the element being moved if it's on the same track
443+ if (
444+ dragState . trackId === track . id &&
445+ existingElement . id === dragState . elementId
446+ ) {
447+ return false ;
448+ }
449+ const existingStart = existingElement . startTime ;
450+ const existingEnd =
451+ existingElement . startTime +
452+ ( existingElement . duration -
453+ existingElement . trimStart -
454+ existingElement . trimEnd ) ;
455+ return (
456+ finalTime < existingEnd && movingElementEnd > existingStart
457+ ) ;
458+ } )
459+ : false ; // If target track not found, allow drop (shouldn't happen)
299460
300461 if ( ! hasOverlap ) {
301462 if ( dragState . trackId === track . id ) {
@@ -313,6 +474,7 @@ export function TimelineTrackContent({
313474 ) ;
314475 }
315476 } else {
477+ // Moving to different track - handle the move
316478 moveElementToTrack (
317479 dragState . trackId ,
318480 track . id ,
@@ -333,17 +495,29 @@ export function TimelineTrackContent({
333495 ) ;
334496 }
335497 } ) ;
498+ // End drag since we handled the drop on this track
499+ endDragAction ( ) ;
500+ onSnapPointChange ?.( null ) ;
501+ return ; // Don't let source track also handle this
336502 }
337503 }
338504 }
339505 } else if ( isTrackThatStartedDrag ) {
506+ // PRIORITY 3: Handle drop on source track
340507 // Mouse is not over this track, but this track started the drag
341- // This means user released over ruler/outside - update position within same track
508+ // Check if element still exists in this track (might have been moved to another track)
342509 const sourceTrack = tracks . find ( ( t ) => t . id === dragState . trackId ) ;
343510 const movingElement = sourceTrack ?. elements . find (
344511 ( c ) => c . id === dragState . elementId
345512 ) ;
346513
514+ // If element no longer exists in source track, it was moved - don't process
515+ if ( ! movingElement ) {
516+ endDragAction ( ) ;
517+ onSnapPointChange ?.( null ) ;
518+ return ;
519+ }
520+
347521 if ( movingElement ) {
348522 const movingElementDuration =
349523 movingElement . duration -
@@ -383,6 +557,9 @@ export function TimelineTrackContent({
383557 // Clear snap point when drag ends
384558 onSnapPointChange ?.( null ) ;
385559 }
560+
561+ // Clear drag overlay
562+ setDragOverlay ( null ) ;
386563 } ;
387564
388565 document . addEventListener ( "mousemove" , handleMouseMove ) ;
@@ -930,8 +1107,8 @@ export function TimelineTrackContent({
9301107 const isCompatible = isVideoOrImage
9311108 ? canElementGoOnTrack ( "media" , track . type )
9321109 : isAudio
933- ? canElementGoOnTrack ( "media" , track . type )
934- : false ;
1110+ ? canElementGoOnTrack ( "media" , track . type )
1111+ : false ;
9351112
9361113 let targetTrack = tracks . find ( ( t ) => t . id === targetTrackId ) ;
9371114
@@ -1106,6 +1283,7 @@ export function TimelineTrackContent({
11061283
11071284 return (
11081285 < div
1286+ ref = { trackContainerRef }
11091287 className = "w-full h-full hover:bg-muted/20"
11101288 onClick = { ( e ) => {
11111289 // If clicking empty area (not on an element), deselect all elements
@@ -1122,6 +1300,29 @@ export function TimelineTrackContent({
11221300 ref = { timelineRef }
11231301 className = "h-full relative track-elements-container min-w-full"
11241302 >
1303+ { /* Drag Overlay - shows where element will be dropped */ }
1304+ { dragOverlay ?. show &&
1305+ dragOverlay . trackId === track . id &&
1306+ dragOverlay . time !== null && (
1307+ < div
1308+ className = "absolute top-0 h-full pointer-events-none z-40"
1309+ style = { {
1310+ left : `${
1311+ dragOverlay . time *
1312+ TIMELINE_CONSTANTS . PIXELS_PER_SECOND *
1313+ zoomLevel
1314+ } px`,
1315+ width : `${ Math . max (
1316+ TIMELINE_CONSTANTS . ELEMENT_MIN_WIDTH ,
1317+ dragOverlay . duration *
1318+ TIMELINE_CONSTANTS . PIXELS_PER_SECOND *
1319+ zoomLevel
1320+ ) } px`,
1321+ } }
1322+ >
1323+ < div className = "h-full border-2 border-dashed border-primary/60 bg-primary/5 rounded-[0.5rem]" />
1324+ </ div >
1325+ ) }
11251326 { track . elements . length === 0 ? (
11261327 < div
11271328 className = { `h-full w-full rounded-sm border-2 border-dashed flex items-center justify-center text-xs text-muted-foreground transition-colors ${
0 commit comments