Adding data from a slightly different scenario that seems to come from the same AppKit momentum logic.
Setup: MacBook Air M4 (Mac16,12), macOS 27.2 (26B123), built-in trackpad. Minimal test app: a plain NSScrollView with a 20,000 pt document view, no subclassing. I logged scroll events at a session event tap plus the clip view origin on every display frame. A plain NSView that only sums scrollingDeltaY never shows the problem.
Scenario: scroll slowly, bring the fingers to a full stop, keep them on the trackpad for about 150 to 250 ms, then lift.
The system sends scrollPhase = ended and no momentum events at all, which is correct because the fingers were stationary.
AppKit still starts its own momentum animation. The content keeps moving 25 to 150 pt after the last event, always in the direction of the gesture. The first frame moves 6 to 15 pt.
Under lldb I see the following:
By default, every position change comes from -[_NSScrollingConcurrentMainThreadSynchronizer _scrollToCanonicalOrigin]. Forcing concurrent scrolling off (making _canOptInToConcurrentScrolling / _isConcurrentScrollingCompatible return NO) switches to NSScrollingBehaviorSingleThreadedVBL, and the jump stays the same.
In single-threaded mode, -[NSScrollingBehaviorSingleThreadedVBL scrollingAnimator:beginMomentumWithVelocity:target:] is called at lift-off with a velocity of 686 to 1421 pt/s. That velocity comes from the middle of the gesture, even though the fingers had not moved for 150+ ms. The target it is given matches where the content ends up.
The velocity estimate looks count-based, not time-based. Zero-delta "changed" events are ignored. If I inject extra 1 pt "changed" events before "ended", the velocity drops: 3 events give 200 to 480 pt/s, 12 events
Topic:
UI Frameworks
SubTopic:
AppKit
Tags: