@@ -95,6 +95,13 @@ type DragState = {
9595 startX : number ;
9696 fromClipPct : number ;
9797 moved : boolean ;
98+ /** Latest pointer x, flushed to the preview once per frame. */
99+ lastX : number ;
100+ /** Index in the sorted row, needed by the neighbour clamp off the render path. */
101+ index : number ;
102+ /** Escape was pressed: the drag is dead, and the pointerup that follows is
103+ * swallowed rather than falling through to the click branch. */
104+ cancelled ?: boolean ;
98105} ;
99106
100107function keyframeTarget (
@@ -150,6 +157,31 @@ export const TimelineDiamondLane = memo(function TimelineDiamondLane({
150157 // (that optimistic hold was the #1763 flake). The atomic move-keyframe commit
151158 // on drop re-keys the diamond from source.
152159 const [ preview , setPreview ] = useState < { kfKey : string ; clipPct : number } | null > ( null ) ;
160+ // One preview render per frame: a 120Hz trackpad fires pointermove far faster
161+ // than the lane can repaint, and every diamond in the row re-evaluates its
162+ // memo on each of those renders.
163+ const previewFrameRef = useRef < number | null > ( null ) ;
164+ const cancelPreviewFrame = ( ) => {
165+ if ( previewFrameRef . current === null ) return ;
166+ cancelAnimationFrame ( previewFrameRef . current ) ;
167+ previewFrameRef . current = null ;
168+ } ;
169+ // Escape backs out of an in-flight retime, the way clip and element drags
170+ // already do. Nothing was written yet (the commit happens on pointerup), so
171+ // dropping the preview is the whole undo.
172+ useEffect ( ( ) => {
173+ const onKeyDown = ( event : KeyboardEvent ) => {
174+ if ( event . key !== "Escape" || ! dragRef . current || dragRef . current . cancelled ) return ;
175+ dragRef . current . cancelled = true ;
176+ cancelPreviewFrame ( ) ;
177+ setPreview ( null ) ;
178+ } ;
179+ document . addEventListener ( "keydown" , onKeyDown ) ;
180+ return ( ) => {
181+ document . removeEventListener ( "keydown" , onKeyDown ) ;
182+ cancelPreviewFrame ( ) ;
183+ } ;
184+ } , [ ] ) ;
153185 // Index of the segment whose mid-point ease button is revealed on hover, like
154186 // Figma. Null = no segment hovered → no button shown (resting state is just
155187 // the connector line + diamonds).
@@ -310,33 +342,47 @@ export const TimelineDiamondLane = memo(function TimelineDiamondLane({
310342 dragRef . current = {
311343 kfKey,
312344 startX : e . clientX ,
345+ lastX : e . clientX ,
346+ index : i ,
313347 fromClipPct : pendingRetimeRef . current . get ( kfKey ) ?. clipPct ?? kf . percentage ,
314348 moved : false ,
315349 } ;
316350 }
317351 } ;
318352 const onPointerMove = ( e : React . PointerEvent < HTMLButtonElement > ) => {
319353 const d = dragRef . current ;
320- if ( ! d || d . kfKey !== kfKey ) return ;
354+ if ( ! d || d . kfKey !== kfKey || d . cancelled ) return ;
355+ d . lastX = e . clientX ;
321356 if ( ! d . moved && Math . abs ( e . clientX - d . startX ) >= KEYFRAME_DRAG_THRESHOLD_PX ) {
322357 d . moved = true ;
323358 }
324- if ( d . moved ) {
359+ if ( ! d . moved || previewFrameRef . current !== null ) return ;
360+ previewFrameRef . current = requestAnimationFrame ( ( ) => {
361+ previewFrameRef . current = null ;
362+ const live = dragRef . current ;
363+ if ( ! live || live . kfKey !== kfKey || live . cancelled ) return ;
325364 setPreview ( {
326365 kfKey,
327366 clipPct : previewClipPct ( {
328- pointerDownX : d . startX ,
329- pointerMoveX : e . clientX ,
367+ pointerDownX : live . startX ,
368+ pointerMoveX : live . lastX ,
330369 clipWidthPx,
331- draggedClipPct : d . fromClipPct ,
332- draggedIndex : i ,
370+ draggedClipPct : live . fromClipPct ,
371+ draggedIndex : live . index ,
333372 sortedClipPcts,
334373 } ) ,
335374 } ) ;
336- }
375+ } ) ;
337376 } ;
338377 const onPointerUp = ( e : React . PointerEvent < HTMLButtonElement > ) => {
339378 const d = dragRef . current ;
379+ if ( d ?. kfKey === kfKey && d . cancelled ) {
380+ // Escape already ended this drag; the release is not a click.
381+ dragRef . current = null ;
382+ e . currentTarget . releasePointerCapture ?.( e . pointerId ) ;
383+ suppressNextClick ( ) ;
384+ return ;
385+ }
340386 // No drag armed (canDrag false / non-primary press) → treat as a click.
341387 if ( ! d || d . kfKey !== kfKey ) {
342388 if ( e . button !== 0 ) return ;
@@ -347,9 +393,14 @@ export const TimelineDiamondLane = memo(function TimelineDiamondLane({
347393 }
348394 e . stopPropagation ( ) ;
349395 dragRef . current = null ;
396+ cancelPreviewFrame ( ) ;
350397 setPreview ( null ) ;
351398 e . currentTarget . releasePointerCapture ?.( e . pointerId ) ;
352399 suppressNextClick ( ) ;
400+ // Single-diamond retime by design: a multi-select drag would have to
401+ // move every selected keyframe as one mutation, which the script ops
402+ // do not express yet. Selecting several and dragging one moves only
403+ // the dragged one.
353404 const res = resolveKeyframeDrag ( {
354405 pointerDownX : d . startX ,
355406 pointerUpX : e . clientX ,
@@ -447,6 +498,7 @@ export const TimelineDiamondLane = memo(function TimelineDiamondLane({
447498 // stays stuck at the last previewed position.
448499 if ( dragRef . current ?. kfKey !== kfKey ) return ;
449500 dragRef . current = null ;
501+ cancelPreviewFrame ( ) ;
450502 setPreview ( null ) ;
451503 e . currentTarget . releasePointerCapture ?.( e . pointerId ) ;
452504 } }
0 commit comments