How to Implement 1:1 Pointer Tracking with Velocity Calculation for Gestures

Use setPointerCapture to track pointer movement outside element bounds, maintain a rolling history of recent positions, and compute release velocity by dividing position delta by elapsed time before passing it to a spring animation.

Smooth, physics-driven gestures require continuous 1:1 pointer tracking and accurate velocity extraction at release. The emilkowalski/skills repository documents a battle-tested pattern for implementing this in web applications, combining pointer capture APIs with spring-based animations to create interfaces that feel truly responsive and alive.

Core Concepts from the Skills Repository

The implementation rests on four interconnected principles documented across the repository's skill files:

Concept Implementation Source
Pointer capture with setPointerCapture() to receive events beyond element bounds skills/apple-design/SKILL.md
Position history buffer for velocity calculation skills/apple-design/SKILL.md
Spring animation with initial velocity for momentum continuation skills/apple-design/SKILL.md
Canonical spring configuration for gesture-driven motion skills/review-animations/STANDARDS.md

Step-by-Step Implementation

1: Capture the Pointer on Gesture Start

Call setPointerCapture() immediately in your pointerdown handler. This ensures the element continues receiving pointermove events even if the cursor leaves its bounds—a critical requirement for 1:1 tracking that doesn't break when users drag quickly.

element.addEventListener('pointerdown', (e) => {
  element.setPointerCapture(e.pointerId);
  // Initialize tracking state...
});

According to skills/apple-design/SKILL.md, this pattern is essential for drag-to-dismiss and flick gestures where the user needs freedom of movement without the gesture terminating prematurely【apple‑design SKILL.md†L42-L47】.

2: Maintain a Rolling Position History

Store timestamped positions in a fixed-length buffer during pointermove. The repository recommends keeping approximately 5 recent samples to balance noise reduction with responsiveness【apple‑design SKILL.md†L43-L46】.

const HISTORY_SIZE = 5;
const history = []; // { t: timestamp, x: number, y: number }

element.addEventListener('pointermove', (e) => {
  history.push({ t: e.timeStamp, x: e.clientX, y: e.clientY });
  if (history.length > HISTORY_SIZE) history.shift();
  
  // Apply 1:1 transform...
  element.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
});

3: Calculate Velocity at Gesture End

Compute velocity by fitting a linear slope through your history buffer. The simplest reliable method: divide the position delta between first and last samples by their time difference.

element.addEventListener('pointerup', () => {
  const [first, last] = [history[0], history[history.length - 1]];
  const dt = (last.t - first.t) / 1000; // seconds
  const dx = last.x - first.x;
  const dy = last.y - first.y;
  
  const vx = dx / dt; // pixels per second
  const vy = dy / dt;
});

4: Launch Spring with Extracted Velocity

Pass the computed velocity to your spring animation. The repository specifies a canonical configuration for gesture-driven springs: { type: 'spring', duration: 0.5, bounce: 0.2 }【STANDARDS.md†L67-L70】.

import { animate } from 'motion';

animate(
  element,
  { x: targetX, y: targetY },
  {
    type: 'spring',
    duration: 0.5,
    bounce: 0.2,
    velocity: { x: vx, y: vy } // Momentum continuation
  }
);

Complete Vanilla JavaScript Implementation

This self-contained example implements the full pipeline with the Motion library:

import { animate } from "https://cdn.skypack.dev/motion";

const el = document.querySelector(".draggable");
let history = null;

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

// ---------- Track movement ----------
el.addEventListener("pointermove", (e) => {
  if (!history) return;
  history.push({ t: e.timeStamp, x: e.clientX, y: e.clientY });
  if (history.length > 5) history.shift();

  // 1:1 pointer tracking
  el.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
});

// ---------- Gesture end with velocity calculation ----------
el.addEventListener("pointerup", (e) => {
  if (!history) return;
  
  const [prev, last] = [history[0], history[history.length - 1]];
  const dt = (last.t - prev.t) / 1000;
  const dx = last.x - prev.x;
  const dy = last.y - prev.y;
  const vx = dx / dt;
  const vy = dy / dt;

  // Spring with momentum continuation
  animate(
    el,
    { x: 0, y: 0 }, // Snap-back target
    {
      type: "spring",
      bounce: 0.2,
      duration: 0.5,
      velocity: { x: vx, y: vy },
    }
  );
  
  history = null;
});

React Hook Implementation

For React applications, encapsulate the pattern in a reusable hook with Framer Motion:

import { useRef } from "react";
import { motion, useMotionValue, animate } from "framer-motion";

export function usePointerGesture() {
  const x = useMotionValue(0);
  const y = useMotionValue(0);
  const history = useRef<Array<{t:number;x:number;y:number}>>([]);

  const start = (e: React.PointerEvent) => {
    (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
    history.current = [{ t: e.timeStamp, x: e.clientX, y: e.clientY }];
  };

  const move = (e: React.PointerEvent) => {
    history.current.push({ t: e.timeStamp, x: e.clientX, y: e.clientY });
    if (history.current.length > 5) history.current.shift();
    x.set(e.clientX);
    y.set(e.clientY);
  };

  const end = () => {
    const h = history.current;
    if (h.length < 2) return;
    
    const dt = (h[h.length - 1].t - h[0].t) / 1000;
    const vx = (h[h.length - 1].x - h[0].x) / dt;
    const vy = (h[h.length - 1].y - h[0].y) / dt;

    animate(y, 0, {
      type: "spring",
      bounce: 0.2,
      duration: 0.5,
      velocity: vy,
    });
  };

  return { x, y, start, move, end };
}

export default function DraggableBox() {
  const { x, y, start, move, end } = usePointerGesture();
  
  return (
    <motion.div
      style={{ x, y, touchAction: "none", position: "absolute" }}
      onPointerDown={start}
      onPointerMove={move}
      onPointerUp={end}
      onPointerCancel={end}
    />
  );
}

Why Velocity-Preserving Springs Matter

The repository emphasizes interruptibility as a core principle for gesture-driven interfaces【apple‑design SKILL.md†L33-L35】. When a spring animation receives the release velocity:

  • No jarring stops — motion continues naturally from the finger's speed
  • Reversible gestures — users can interrupt and reverse direction smoothly
  • iOS-native feel — matches "additive animations" in Apple's interface patterns【apple‑design SKILL.md†L62-L66】

Key Source Files

File Purpose
skills/apple-design/SKILL.md Pointer capture, velocity history, spring integration
skills/review-animations/STANDARDS.md Canonical spring configs: duration: 0.5, bounce: 0.2
skills/animation-vocabulary/SKILL.md Spring terminology: stiffness, damping, critical damping, momentum scenarios
skills/pick-ui-library/SKILL.md Motion/Framer Motion library recommendations

Summary

  • Capture with setPointerCapture() to track beyond element bounds
  • Buffer the last 5 positions with timestamps during movement
  • Calculate velocity as Δposition/Δtime from history samples
  • Animate with springs using the repository's standard config: duration: 0.5, bounce: 0.2
  • Preserve momentum by passing computed velocity to the spring's velocity option

Frequently Asked Questions

How large should the position history buffer be?

Five samples provides optimal balance. Fewer samples amplify noise from input jitter; more samples add latency and may capture outdated momentum. The skills/apple-design/SKILL.md documentation specifically references this approximate window for velocity calculation【apple‑design SKILL.md†L43-L46】.

Can I use requestAnimationFrame instead of pointer events for tracking?

Pointer events via setPointerCapture are required for reliable 1:1 tracking that continues outside element bounds. requestAnimationFrame polling loses pointer lock and cannot capture movement beyond the original element's geometry. The repository explicitly recommends pointer capture APIs for this reason【apple‑design SKILL.md†L42-L47】.

What spring parameters work best for heavy vs. light elements?

Use the canonical config (bounce: 0.2, duration: 0.5) as your baseline. Adjust bounce higher (0.3-0.4) for lighter feel, lower (0.0-0.1) for heavier, more substantial motion. The skills/animation-vocabulary/SKILL.md file defines spring taxonomy for tuning these values to specific momentum scenarios【animation‑vocabulary SKILL.md†L126-L131】.

How do I handle pointercancel events?

Treat pointercancel identically to pointerup: compute velocity from available history and launch the spring. This ensures the gesture completes gracefully if the browser interrupts the pointer stream (system gesture, touch-action conflict, etc.).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →