# How to Implement Velocity‑Based Gesture Dismissal with Momentum Projection

> Implement velocity-based gesture dismissal with momentum projection using exponential decay for fluid, interruptible animations. Learn how to pass release velocity to spring animations.

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

---

**Pass the pointer's release velocity directly to a spring animation and project the resting position using exponential decay to create fluid, interruptible gesture dismissal.**

Velocity‑based gesture dismissal with momentum projection creates interfaces that feel physically responsive—elements that can be "thrown" and caught mid‑flight. This article draws from the Apple Design skill in `emilkowalski/skills` to show you how to capture pointer history, compute release velocity, and hand it off to a spring for natural deceleration.

---

## Capture Pointer History for Accurate Velocity

The foundation of momentum gestures is a **sliding window of recent pointer events**. Per [`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md), you must "track a short velocity/position history" to avoid noisy velocity calculations from single‑sample deltas.

Store the last 5–10 `pointermove` events with timestamps:

```javascript
let history = []; // {x, y, time}[]
const MAX_HISTORY = 5;

function recordPointer(e) {
  const now = performance.now();
  history.push({ x: e.clientX, y: e.clientY, time: now });
  if (history.length > MAX_HISTORY) history.shift();
}

```

This capped array ensures velocity reflects recent motion intent rather than stale positions.

---

## Compute Release Velocity at Pointer Up

On `pointerup`, calculate velocity from the final two samples. The Apple Design skill emphasizes using the **exact release velocity** to avoid visual seams between gesture and animation.

```javascript
function getReleaseVelocity() {
  if (history.length < 2) return 0;
  
  const [prev, last] = history.slice(-2);
  const dt = (last.time - prev.time) || 1; // ms
  
  const vx = (last.x - prev.x) / dt; // px/ms
  const vy = (last.y - prev.y) / dt;
  
  return {
    x: vx * 1000, // convert to px/s for Motion
    y: vy * 1000,
    magnitude: Math.hypot(vx, vy) * 1000
  };
}

```

Multiply by 1000 to match Motion's **pixel‑per‑second** velocity units.

---

## Project Momentum to Find the Resting Target

Raw release position alone produces abrupt stops. Instead, **project where velocity would carry the element** under exponential decay, then snap to the nearest valid boundary.

[`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md) provides the reference projection function using `decelerationRate`:

```javascript
const DECELERATION_RATE = 0.998; // UIScrollView‑like feel

function projectDistance(velocityPxPerMs) {
  // Exponential decay: distance = (v / 1000) * r / (1 - r)
  return (velocityPxPerMs / 1000) * DECELERATION_RATE / (1 - DECELERATION_RATE);
}

function getTargetY(currentY, velocityY) {
  const projected = projectDistance(Math.abs(velocityY) / 1000);
  const direction = Math.sign(velocityY);
  const restingY = currentY + (projected * direction);
  
  return nearestSnapPoint(restingY); // your snap logic
}

```

This projection ensures the element "overshoots" naturally when thrown fast, matching native iOS sheet behavior.

---

## Hand Off Velocity to a Spring Animation

The critical hand‑off: pass the measured velocity as the spring's initial condition. The Apple Design skill states the spring must "start from the presentation value and receive the finger's exact velocity."

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

function dismissSheet(element, startY, velocity) {
  const targetY = getTargetY(startY, velocity.y);
  
  animate(element, { y: targetY }, {
    type: 'spring',
    
    // Stiffness/damping tuned for gesture momentum
    stiffness: 300,
    damping: 25,
    
    // Hand off the exact release velocity
    velocity: velocity.y,
    
    // Add bounce only for momentum gestures (not taps)
    bounce: Math.abs(velocity.y) > 500 ? 0.2 : 0
  });
}

```

Notice the **conditional bounce**: [`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md) warns to "add bounce only when the gesture itself carried momentum"—fast swipes feel lively, slow drags settle quietly.

---

## Complete Working Implementation

Combine all stages into a reusable gesture controller:

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

function createMomentumDismissal(element, options = {}) {
  const {
    snapPoints = [0, 300, 600], // Y positions
    decelerationRate = 0.998,
    maxHistory = 5
  } = options;
  
  let history = [];
  let currentY = 0;
  let isDragging = false;
  
  const record = (e) => {
    history.push({
      x: e.clientX,
      y: e.clientY,
      time: performance.now()
    });
    if (history.length > maxHistory) history.shift();
  };
  
  const nearestSnap = (y) => snapPoints.reduce((closest, p) => 
    Math.abs(p - y) < Math.abs(closest - y) ? p : closest
  );
  
  const project = (v) => (v / 1000) * decelerationRate / (1 - decelerationRate);
  
  element.addEventListener('pointerdown', (e) => {
    isDragging = true;
    element.setPointerCapture(e.pointerId);
    animate(element, { y: currentY }, { duration: 0 }); // stop any animation
    history = [];
    record(e);
  });
  
  element.addEventListener('pointermove', (e) => {
    if (!isDragging) return;
    record(e);
    
    // Direct manipulation: follow finger 1:1
    const dy = e.clientY - history[0].y;
    currentY = Math.max(0, dy); // resist pulling up
    element.style.transform = `translateY(${currentY}px)`;
  });
  
  element.addEventListener('pointerup', (e) => {
    if (!isDragging) return;
    isDragging = false;
    record(e);
    
    // Compute velocity from last two samples
    const [a, b] = history.slice(-2);
    const dt = Math.max(1, b.time - a.time);
    const vy = ((b.y - a.y) / dt) * 1000; // px/s
    
    // Project and snap
    const projectedY = currentY + Math.sign(vy) * project(Math.abs(vy) / 1000);
    targetY = nearestSnap(projectedY);
    
    // Spring with velocity hand-off
    animate(element, { y: targetY }, {
      type: 'spring',
      stiffness: 300,
      damping: 25,
      velocity: vy,
      bounce: Math.abs(vy) > 500 ? 0.2 : 0
    }).then(() => { currentY = targetY; });
  });
}

```

---

## Key Design Principles from the Source

| Principle | Source Reference | Implementation Impact |
|-----------|------------------|----------------------|
| Track short velocity history | [`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md) lines 42–50 | `maxHistory: 5` prevents noise |
| Exact velocity hand‑off | [`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md) lines 100–108 | `velocity: vy` in spring config |
| Exponential decay projection | [`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md) lines 121–128 | `project()` function for target calculation |
| Conditional bounce | [`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md) lines 76–79 | `bounce: 0.2` only when `\|vy\| > 500` |

---

## Summary

- **Capture 5–10 recent pointer events** with timestamps to compute stable release velocity.
- **Calculate velocity in px/s** from the final two samples, handling edge cases like single‑tap releases.
- **Project the resting position** using exponential decay (`decelerationRate ≈ 0.998`) rather than snapping from the release point.
- **Hand exact velocity to the spring**—the animation inherits momentum and remains interruptible.
- **Apply bounce selectively** to distinguish momentum gestures from precise positioning.

---

## Frequently Asked Questions

### Why track multiple pointer events instead of just the last one?

Single‑sample velocities are noisy due to input latency and sub‑pixel movement. The Apple Design skill recommends "a short velocity/position history" (lines 42–50) to average out jitter while remaining responsive to intentional fast swipes.

### What deceleration rate should I use for momentum projection?

A `decelerationRate` of 0.998 matches **UIScrollView physics** and provides familiar iOS‑like feel. Lower values (0.99) stop faster; higher values (0.9995) glide longer. Adjust based on your element's perceived mass.

### Can I use this with libraries other than Motion?

Yes—the pattern is framework‑agnostic. Pass velocity to **Framer Motion's** `animate()` with `velocity`, **React Spring's** `useSpring` with `config.velocity`, or **WAAPI** with custom spring integration. The core requirement remains: hand off the exact release velocity to maintain momentum continuity.

### How do I make the gesture reversible mid‑animation?

Springs are inherently **interruptible**. On a new `pointerdown`, call `animate(element, { y: currentY }, { duration: 0 })` to stop the spring at its present value, then resume tracking. The velocity history resets, but the element never snaps—creating seamless grab‑and‑throw behavior.