# `` _(no submodules)_ ## Purpose Velocity-sensitive drum pad. Maps a pointer or keyboard press to a velocity 0..1 then through `[min, max]` so the same component can serve as a 0..127 MIDI velocity emitter, a 0..1 normalized parameter trigger, or anything in between. Velocity sourcing is layered: hardware `PointerEvent.pressure` is preferred when the device supplies a non-zero value (Apple Pencil, pressure-sensitive trackpads, ChromeOS active styluses); otherwise a duration heuristic measures press hold time and produces velocity via a caller-replaceable function. ## Features - Two velocity sources, transparently selected: hardware pressure or press-duration heuristic. - Live pressure tracking: `pointermove` updates velocity continuously while pressure is supplied (useful for aftertouch-style modulation). - Keyboard activation (Space, Enter) using the duration heuristic. - Auto-repeat keyboard suppression — held keys don't re-arm the timer. - Pointer capture for drift-tolerant release. - Strike-then-release pattern: caller receives `(velocity)` followed immediately by `min` so downstream consumers can treat each press as a discrete event without bookkeeping. - Injectable `nowMs` clock for test determinism. - `role="button"` with live `aria-pressed`. - CSS-variable theming (`--cs-pad-idle`, `--cs-pad-pressed`, `--cs-pad-border`, `--cs-pad-label-color`). ## Usage: Basic ```tsx import { useState } from "react"; import { DrumPad } from "@suaveplan/react-control-surface"; export function KickPad() { const [v, setV] = useState(0); return ( ); } ``` ## Usage: Advanced A 4×4 pad grid with a custom velocity curve and shared state: ```tsx import { useState } from "react"; import { DrumPad } from "@suaveplan/react-control-surface"; const LABELS = [ "KICK","SNARE","CLAP","RIM", "OH","CH","RIDE","CRASH", "TOM1","TOM2","TOM3","PERC", "FX1","FX2","FX3","FX4", ]; // Aggressive curve: 80 ms → max velocity. const curve = (ms: number) => Math.min(1, ms / 80) ** 0.7; export function Grid() { const [values, setValues] = useState(() => LABELS.map(() => 0)); return (
{LABELS.map((lbl, i) => ( { const next = values.slice(); next[i] = v; setValues(next); }} min={0} max={127} velocityFromDurationMs={curve} /> ))}
); } ``` ## API Reference | Prop | Type | Default | | ------------------------ | ----------------------------- | ------------------------ | | `value` | `number` | required | | `onChange` | `(v: number) => void` | required | | `min`/`max` | `number` | required | | `disabled` | `boolean` | `false` | | `label` | `string` | — | | `ariaLabel` | `string` | — | | `displayLabel` | `string` | — | | `widthPx` | `number` | `80` | | `heightPx` | `number` | `80` | | `velocityFromDurationMs` | `(ms: number) => number` | `min(1, ms / 200)` | | `nowMs` | `() => number` | `performance.now` | | `className` | `string` | — | ## Implementation Notes The strike + release pattern uses `queueMicrotask` so consumers see two distinct emissions in order: first the velocity at strike, then `min` immediately after. This keeps downstream parameter-bound state trivially correct (no manual reset) while preserving the strike event for envelope retriggering. Keyboard auto-repeat suppression checks the active-press ref before re-arming so a held key produces a single sustained press rather than a stream of strikes. The injectable `nowMs` exists exclusively so tests can run with a fake clock and produce deterministic velocity values without relying on real wall-clock timing.