Files
skills/docs/ui/react/react-control-surface/src/controls/jog-wheel/jog-wheel.md
2026-05-29 08:20:56 +01:00

4.2 KiB

<JogWheel> — endless rotary with velocity + inertia

(no submodules)

Purpose

<JogWheel> is an endless rotary control inspired by DJ-deck jog wheels. Unlike a knob, it does not have an absolute value — instead each pointer-drag event emits a delta in radians via onChange, which the caller integrates into whatever parameter the wheel controls (scrub position, tempo bend, parameter trim, etc.).

The wheel reads pointer motion tangentially around its centre: motion that curls clockwise around the hub produces positive deltas, motion curling counter-clockwise produces negative deltas. On release, if the angular velocity exceeds inertiaThreshold, the wheel continues to spin under exponential friction (inertiaFriction) and emits deltas via requestAnimationFrame until the velocity falls below a small cut-off.

Features

  • Tangential pointer-motion integration via 2D cross product
  • Pointer-capture so drags continuing off the wheel still register
  • Configurable sensitivity (radiansPerPixel)
  • Inertial spin after release, with caller-tunable threshold + friction
  • Tab focus + arrow-key navigation (±0.1 rad per arrow press)
  • role="slider" with descriptive aria-valuetext
  • data-spinning attribute for E2E and CSS visual feedback

Usage: Basic

import { useState } from "react";
import { JogWheel } from "@suaveplan/react-control-surface";

function Scrubber() {
    const [pos, setPos] = useState(0);
    return (
        <JogWheel onChange={(d) => setPos((p) => p + d)} label="Scrub" />
    );
}

Usage: Advanced

import { JogWheel } from "@suaveplan/react-control-surface";

function PitchBend({ engine }: { engine: { bend: (cents: number) => void } }) {
    return (
        <JogWheel
            onChange={(delta) => engine.bend(delta * 60)}
            radiansPerPixel={0.04}
            inertiaThreshold={2}
            inertiaFriction={3.5}
            size={180}
            label="Pitch bend"
        />
    );
}

API Reference

Prop Type Default Description
onChange (deltaRadians: number) => void req Called per drag step + inertia tick.
radiansPerPixel number 0.02 Sensitivity (≈1.15 deg / pixel by default).
inertiaThreshold number 4 Min release velocity (rad/s) to start spinning.
inertiaFriction number 2.5 Exponential decay coefficient (per second).
size number 120 Render size in CSS pixels (the wheel is square).
disabled boolean false When true, ignores all input.
label string — Visible + accessible label.
ariaLabel string derived Override the accessible label.
className string — Appended to rcs-jog-wheel.

The exported decayVelocity(velocity, friction, dtMs) helper is the pure exponential-decay primitive used internally.

Implementation Notes

The tangential pixel motion is computed as (rx * dy - ry * dx) / radius, i.e. the 2D cross product of the radius vector and the motion vector divided by the radius magnitude. This produces a signed scalar that is positive for clockwise motion (in standard screen-coordinates where Y points down) and negative for counter-clockwise. We guard against radius-zero starts by skipping motion samples that occur within 1 pixel of the centre.

Inertia is driven by a separate useEffect that runs only while spinning is true. The effect captures the current inertiaFriction and emits a delta per frame: step = velocity * dt. Once the absolute velocity falls below 0.05 rad/s, the effect clears its own spinning flag and unmounts the RAF loop.