# `` _(no submodules)_ ## Purpose A single piano key. Two visual kinds (`white` / `black`) and three visual states (`idle` / `pressed` / `highlighted`). Consumers compose many keys to build a full keyboard component — `` itself deliberately renders a single rect so it stays compositional, theming flexible, and reusable in non-piano contexts (e.g. step sequencer cells themed as keys). The component implements `BoundControlProps` and emits `onChange(max)` on press, `onChange(min)` on release. ## Features - Pointer and keyboard activation (Space, Enter). - Pointer-leave release (no stuck keys when you glide off the key). - Key-auto-repeat suppression via active-ref tracking. - Explicit `state` prop overrides derived state so callers can paint a key as `highlighted` (e.g. for chord-shape display) without changing `value`. - Default dimensions tuned per `kind` (white: 32×140, black: 20×90). - `role="button"` with `aria-pressed` reflecting the current state. - CSS-variable theming: `--cs-piano-white`, `--cs-piano-white-pressed`, `--cs-piano-white-highlight`, `--cs-piano-white-stroke`, `--cs-piano-black`, `--cs-piano-black-pressed`, `--cs-piano-black-highlight`, `--cs-piano-black-stroke`. ## Usage: Basic ```tsx import { useState } from "react"; import { PianoKey } from "@suaveplan/react-control-surface"; export function MiddleC() { const [v, setV] = useState(0); return ( ); } ``` ## Usage: Advanced Compose an octave with black keys overlaid on whites: ```tsx import { useState } from "react"; import { PianoKey } from "@suaveplan/react-control-surface"; export function Octave() { const [pressed, setPressed] = useState>(new Set()); const flip = (n: number, on: boolean) => setPressed((p) => { const next = new Set(p); if (on) next.add(n); else next.delete(n); return next; }); const whites = [0, 2, 4, 5, 7, 9, 11]; const blacks = [{ off: 1, x: 22 }, { off: 3, x: 54 }, { off: 6, x: 118 }]; return (
{whites.map((o, i) => (
flip(60 + o, v > 0)} min={0} max={1} note={60 + o} />
))} {blacks.map(({ off, x }) => (
flip(60 + off, v > 0)} min={0} max={1} note={60 + off} />
))}
); } ``` ## API Reference | Prop | Type | Default | | ----------- | ----------------------------- | -------------- | | `value` | `number` | required | | `onChange` | `(v: number) => void` | required | | `min`/`max` | `number` | required | | `disabled` | `boolean` | `false` | | `kind` | `"white" \| "black"` | `"white"` | | `state` | `"idle" \| "pressed" \| "highlighted"` | derived | | `note` | `number` | — | | `label` | `string` | — | | `ariaLabel` | `string` | — | | `widthPx` | `number` | per-kind | | `heightPx` | `number` | per-kind | | `className` | `string` | — | ## Implementation Notes The active state tracks an internal ref rather than React state so the press / release pair is event-loop-stable and immune to auto-repeat storms from held keys. Pointer capture is set on `pointerdown` and released on the matching `pointerup` / `pointercancel` / `pointerleave`, which gives correct release semantics even when the pointer drifts off the SVG (touch glissando). The explicit `state` prop fully overrides the derived state so the component can be driven from an external signal (incoming MIDI from another player, computer-aided visualisation) without coupling `value` to the visual state.