# Implementing Velocity Handoff Between Gesture Drag and Spring Animation: A Complete Guide

> Implement velocity handoff between gesture drag and spring animation for fluid, momentum-driven interfaces. This guide ensures smooth transitions eliminating jarring stops.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: tutorial
- Published: 2026-08-05

---

**Velocity handoff ensures that a spring animation continues with the exact speed of a released drag gesture, eliminating jarring stops and creating fluid, momentum-driven interfaces.**

Fluid gesture-driven interfaces depend on one critical transition: when a user's finger lifts off the screen, the ensuing animation must carry that momentum forward. This article demonstrates how to implement velocity handoff using the Pointer Events API and spring physics, drawing from the implementation patterns documented in the `emilkowalski/skills` repository.

## Capturing Pointer Velocity Before Release

The foundation of velocity handoff lies in accurate velocity measurement during the final moments of a gesture. The **Pointer Events API** provides the precision needed through `setPointerCapture`, which ensures continuous event delivery even when the cursor leaves the target element.

### Recording Motion History

Maintain a short buffer of position-timestamp pairs to compute a smoothed velocity vector:

```javascript
const el = document.querySelector('.draggable');
let history = []; // {x, y, t}[]

el.addEventListener('pointerdown', e => {
  el.setPointerCapture(e.pointerId);
  history = [{x: e.clientX, y: e.clientY, t: e.timeStamp}];
});

el.addEventListener('pointermove', e => {
  history.push({x: e.clientX, y: e.clientY, t: e.timeStamp});
  if (history.length > 5) history.shift();
  
  const dx = e.clientX - history[0].x;
  el.style.transform = `translate(${dx}px, 0)`;
});

```

Key implementation details from [[`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md)](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md#L100-L110):

- **History length**: 3-5 samples balances responsiveness with noise reduction
- **Timestamp precision**: `DOMHighResTimeStamp` via `e.timeStamp` provides sub-millisecond accuracy
- **Capture guarantee**: `setPointerCapture` prevents velocity loss from boundary exits

### Computing Release Velocity

Calculate the velocity vector on `pointerup` using the oldest and newest samples in your buffer:

```javascript
el.addEventListener('pointerup', e => {
  if (history.length < 2) return;
  
  const first = history[0];
  const last = history[history.length - 1];
  const dt = (last.t - first.t) / 1000; // seconds
  
  const vx = (last.x - first.x) / dt;   // pixels per second
  
  startSpringAnimation(el, vx);
});

```

## Passing Velocity to Spring Animation

Spring libraries accept an initial velocity parameter that seeds the physical simulation. The **Motion** library (successor to Framer Motion) implements this through the `velocity` option in `animate()`.

### Basic Velocity Handoff Implementation

```javascript
import { animate } from 'motion';

function startSpringAnimation(element, vx) {
  const targetX = 0; // resting position
  
  animate(element, { x: targetX }, {
    type: 'spring',
    duration: 0.4,
    bounce: 0.2,      // momentum-driven feel per repo standards
    velocity: vx      // ← critical: gesture velocity imported here
  });
}

```

### Normalizing Relative Velocity

Some spring APIs expect velocity relative to the remaining displacement. Per [[`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md)](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md#L100-L110):

```javascript
const currentX = element.getBoundingClientRect().left;
const displacement = targetX - currentX;
const relativeVelocity = vx / displacement; // velocity per unit distance

```

### Recommended Spring Configuration

The repository's animation standards in [[`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md)](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md#L67-L70) specify:

| Property | Value | Purpose |
|----------|-------|---------|
| `duration` | `0.4` | Natural settling time |
| `bounce` | `0.2` | Subtle momentum preservation |
| `type` | `'spring'` | Physical simulation basis |

For **critically-damped** interactions (snapping without oscillation), reduce `bounce` toward `0`. For **under-damped** interactions (playful, bouncy feel), increase toward `0.5+`.

## Ensuring Interruptibility and Continuity

A robust velocity handoff system must handle **interruption**: the user may re-grab the element while the spring is still in motion. Failure to handle this creates the "brick-wall" effect where the element jumps discontinuously.

### Reading Live Presentation Values

Always restart from the **current computed position**, not the animation target:

```javascript
let currentSpring = null;

function startSpringAnimation(element, vx) {
  // Stop ongoing animation but preserve visual state
  if (currentSpring) currentSpring.stop();
  
  // Critical: sample the live rendered position
  const currentX = element.getBoundingClientRect().left;
  
  currentSpring = animate(element, { x: 0 }, {
    type: 'spring',
    duration: 0.4,
    bounce: 0.2,
    velocity: vx,
    onComplete: () => { currentSpring = null; }
  });
}

```

As demonstrated in [[`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md)](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md#L151-L165), the `useSpring` hook in component implementations follows this same pattern—reading the current motion value rather than assuming target completion.

### Decomposing 2D Motion

For diagonal gestures, maintain independent velocity vectors per axis:

```javascript
// From pointer history
const vx = (last.x - first.x) / dt;
const vy = (last.y - first.y) / dt;

// Separate springs preserve axis-specific momentum
animate(element, { x: targetX, y: targetY }, {
  type: 'spring',
  velocity: { x: vx, y: vy }  // Motion accepts per-axis velocities
});

```

Per [[`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md)](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md#L100-L110), this prevents velocity blending artifacts where a fast horizontal swipe with slight vertical drift gets incorrectly averaged.

## Complete Working Example

```javascript
import { animate } from 'motion';

class DraggableWithVelocityHandoff {
  element = null;
  history = [];
  currentSpring = null;
  
  constructor(selector) {
    this.element = document.querySelector(selector);
    this.attachListeners();
  }
  
  attachListeners() {
    const el = this.element;
    
    el.addEventListener('pointerdown', e => {
      el.setPointerCapture(e.pointerId);
      // Interrupt any ongoing spring immediately
      this.currentSpring?.stop();
      
      this.history = [{
        x: e.clientX, 
        y: e.clientY, 
        t: e.timeStamp
      }];
    });
    
    el.addEventListener('pointermove', e => {
      this.history.push({
        x: e.clientX, 
        y: e.clientY, 
        t: e.timeStamp
      });
      if (this.history.length > 5) this.history.shift();
      
      // Direct manipulation: 1:1 tracking
      const dx = e.clientX - this.history[0].x;
      el.style.transform = `translate(${dx}px, 0)`;
    });
    
    el.addEventListener('pointerup', e => {
      if (this.history.length < 2) return;
      
      const v = this.calculateVelocity();
      this.startSpring(v.x);
    });
    
    el.addEventListener('pointercancel', () => {
      this.startSpring(0); // Decay to rest without momentum
    });
  }
  
  calculateVelocity() {
    const first = this.history[0];
    const last = this.history[this.history.length - 1];
    const dt = (last.t - first.t) / 1000;
    
    return {
      x: (last.x - first.x) / dt,
      y: (last.y - first.y) / dt
    };
  }
  
  startSpring(vx) {
    this.currentSpring?.stop();
    
    const currentX = this.element.getBoundingClientRect().left;
    
    this.currentSpring = animate(this.element, { x: 0 }, {
      type: 'spring',
      duration: 0.4,
      bounce: 0.2,
      velocity: vx,
      onComplete: () => { this.currentSpring = null; }
    });
  }
}

// Initialize
new DraggableWithVelocityHandoff('.draggable');

```

## Key Terminology for Configuration

Understanding these terms from [[`skills/animation-vocabulary/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/animation-vocabulary/SKILL.md)](https://github.com/emilkowalski/skills/blob/main/skills/animation-vocabulary/SKILL.md#L126-L130) helps tune your springs:

- **Stiffness**: Spring's resistance to displacement—higher values snap faster
- **Damping**: Energy dissipation rate—controls oscillation decay
- **Bounce**: Convenience parameter mapping to damping ratio; `0` = critically damped
- **Velocity**: Initial speed at animation start—the handoff value from gestures

## Summary

- **Capture velocity** using `setPointerCapture` and a 3-5 sample history buffer to compute release speed accurately
- **Hand off to springs** via the `velocity` parameter in Motion/Framer Motion, normalizing to relative velocity if required by your API
- **Enable interruption** by stopping ongoing springs and restarting from the live `getBoundingClientRect()` position, never the target
- **Decompose 2D motion** into independent per-axis springs to preserve directional momentum fidelity

## Frequently Asked Questions

### What is velocity handoff in gesture animations?

Velocity handoff is the technique of transferring the instantaneous speed of a user's drag gesture directly into a spring animation's initial velocity parameter. This creates continuous motion where the element "coasts" naturally after release rather than starting from zero speed. According to `emilkowalski/skills`, this pattern is essential for Apple-caliber gesture interfaces.

### How do I prevent velocity calculation errors on quick taps?

Require a minimum history length before computing velocity—typically 2-3 samples. For very short interactions, fall back to zero velocity or a default decay. The pointer handler should check `if (history.length < 2) return` and either skip animation or apply a gentle settle-to-rest spring without initial velocity.

### Which spring configuration should I use for different UI patterns?

Per [[`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md)](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md#L67-L70), use `{ bounce: 0.2, duration: 0.4 }` for standard momentum-driven interactions. For destructive confirmations or dismissals, use critically damped springs (`bounce: 0`). For playful, exploratory interfaces, increase bounce toward `0.5` to introduce visible oscillation.

### Can I implement velocity handoff without the Motion library?

Yes. Any spring physics implementation accepting initial velocity works—including custom `requestAnimationFrame` loops solving `F = -kx - cv` numerically, or alternatives like React Spring, Popmotion, or WAAPI with `transition-timing-function`. The critical requirement is injecting the computed `vx/vy` values at animation start time.