# `` — 2-axis touch surface _(no submodules)_ ## Purpose `` is an accessible 2-axis touch surface that emits normalised `{ x, y }` coordinates as the user drags a pointer or steps with the arrow keys. The Y axis follows musical-software convention — the top of the pad is `y = 1` and the bottom is `y = 0`. Both axes are normalised to `[0, 1]` regardless of the pad's pixel size, so the caller can map them onto any parameter range with a simple scale. Common applications include morphing two modulation sources, mapping filter cutoff against resonance, controlling pan + send, or any pair of correlated parameters that benefit from a single gestural surface. ## Features - Pointer-drag emission via Pointer Events with capture for off-pad drags - Keyboard navigation — Arrows step by `step`, PageUp/PageDown by `step * 4`, Home/End jump the X axis to the extremes - Full Tab focusability with `role="application"` - Clamping to `[0, 1]` on every emission, including out-of-bounds drags - `disabled` removes the element from the Tab order and silences events - CSS-variable themed background and cursor - `data-x` / `data-y` deterministic attributes for E2E inspection ## Usage: Basic ```tsx import { useState } from "react"; import { XYPad } from "@suaveplan/react-control-surface"; function Pad() { const [v, setV] = useState({ x: 0.5, y: 0.5 }); return ; } ``` ## Usage: Advanced ```tsx import { useState } from "react"; import { XYPad } from "@suaveplan/react-control-surface"; function FilterPad({ onCutoff, onResonance, }: { onCutoff: (hz: number) => void; onResonance: (q: number) => void; }) { const [v, setV] = useState({ x: 0.5, y: 0.5 }); return ( { setV(next); onCutoff(40 + next.x * 19960); onResonance(0.5 + next.y * 9.5); }} step={0.01} width={320} height={240} label="Filter morph" /> ); } ``` ## API Reference | Prop | Type | Default | Description | | ----------- | ---------------------------------------- | ------- | ------------------------------------------ | | `value` | `{ x: number; y: number }` | req | Current normalised value, both in `[0, 1]`.| | `onChange` | `(next: XYValue) => void` | req | Called for every drag and keyboard step. | | `step` | `number` | `0.05` | Step size for arrow keys. | | `width` | `number` | `200` | Render width in CSS pixels. | | `height` | `number` | `200` | Render height in CSS pixels. | | `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-xy-pad`. | The exported `pointerToValue(rect, clientX, clientY)` helper performs the pure rect-to-`[0, 1]` conversion (including Y-inversion and clamping) and is exported for re-use in custom pads. ## Implementation Notes Pointer events are used uniformly across mouse + touch + pen. On `pointerdown` the pad calls `setPointerCapture` so subsequent drags outside the bounding rect continue to fire `pointermove` events. The `draggingRef` flag avoids spurious moves before the user has pressed. Keyboard handling is exhaustive: every recognised key calls `preventDefault` and emits a clamped value; unrecognised keys do nothing (the event bubbles for the consumer to handle). The `aria-valuetext` attribute is updated on every render with the current `x`/`y` truncated to two decimals, providing screen-reader support without a polling cycle.