Gesture & drag

Teaching your UI to understand flicks, swipes, and drags

Drag requires frame-by-frame response: zero perceptible lag, resistance at boundaries, natural settle on release. ## Pointer capture Without `setPointerCapture`, the element loses track of the cursor the moment it escapes bounds. ```tsx const handlePointerDown = (e: React.PointerEvent) => { (e.target as HTMLElement).setPointerCapture(e.pointerId); // Begin tracking }; ``` Always use `pointer` events, not `mouse` events. Pointer events unify mouse, touch, and stylus. } /> ## Momentum Track the last 2-3 pointer positions with timestamps. Compute velocity as `deltaPosition / deltaTime`. Pass that velocity to a spring. ```tsx // On release, pass the gesture velocity to the spring animate(x, snapPoint, { type: "spring", velocity: gesture.velocity, stiffness: 400, damping: 40, }); ``` Motion's `drag` prop handles this automatically. } /> ## Boundary damping Apply logarithmic damping past the boundary: ```tsx function rubberBand(offset: number, dimension: number, constant = 0.55) { return (offset * dimension * constant) / (dimension + constant * offset); } ``` 0.55 is iOS's value. 0.3 feels stiffer. 0.8 feels loose. ## Swipe-to-dismiss Use both distance and velocity, never just one. ```tsx const shouldDismiss = Math.abs(offset) > dimension * 0.35 || Math.abs(velocity) > 500; ``` If neither is met, spring back. On dismiss, animate off-screen with current velocity. ## Touch-action and scroll conflict ```css /* Horizontal drag: let the browser handle vertical scroll */ .horizontal-drag { touch-action: pan-y; } /* Vertical drag: let the browser handle horizontal scroll */ .vertical-drag { touch-action: pan-x; } /* Full custom gesture: prevent all browser handling */ .custom-gesture { touch-action: none; } ``` ## Axis locking On the first `pointermove`, lock to whichever axis has the larger delta. ```tsx if (!axisLock) { if (Math.abs(deltaX) > Math.abs(deltaY)) { axisLock = "x"; } else { axisLock = "y"; } } ``` Apply a 5-8px dead zone before checking. ## The 60fps requirement - **Only animate `transform` and `opacity`.** - **Use `will-change: transform`** on the dragged element. Remove it when drag ends. - **Avoid React re-renders during drag.** Use `useMotionValue` or refs, not state.