# Keyboard Interaction Contract
Mechanica's 3D viewer must be operable without a pointer, but it must not steal native keyboard behavior from forms, buttons, links, menus, or assistive-technology-driven controls. This contract defines the shortcut scope model, default bindings, and implementation rules used by the new `keyboardShortcuts` utility and `useKeyboardShortcuts` hook.
## Scope model
Keyboard shortcuts are layered by scope:
1. **Global** — available across the app, such as help and dismiss.
2. **Route scope** — active only on a major page (`catalogue`, `viewer`).
3. **Panel/modal scope** — active only while a drawer, modal, or focused composite widget owns interaction.
Conflict detection treats `global` bindings as overlapping every scope. Route-local bindings may reuse the same key if they cannot be active at the same time; for example, a catalogue-only shortcut and viewer-only shortcut can both use `E`, but a global `E` would conflict with both.
## Safety rules
- Never hijack `Tab`; browser focus order remains the primary keyboard navigation mechanism.
- Do not intercept `Space` or `Enter` while focus is on a native/semantic button, link, menu item, tab, slider, tree item, grid, or similar interactive control.
- Do not intercept printable shortcuts while focus is in text inputs, textareas, selects, contenteditable regions, or ARIA text entry roles.
- Do not match shortcuts during `AltGraph` composition, because many international keyboard layouts emit `Ctrl+Alt` while typing characters.
- Repeating keydown events are ignored by default. Continuous controls such as stepping animation or adjusting exploded separation must opt in with `allowRepeat: true`.
- Escape is the exception for modal/drawer dismissal and may opt into `allowInEditable` and `allowInInteractive` when the active overlay owns focus.
For edge cases, focused elements or ancestors may set:
```html
...
```
The nearest policy wins. Use `allow` sparingly; it is intended for the focused viewer canvas or a composite widget that has implemented its own keyboard semantics.
## Default binding catalogue
| Scope | Shortcut | Action |
| --- | --- | --- |
| Global | `?` (`Shift + /`) | Open the keyboard shortcut reference |
| Global | `Escape` | Dismiss the active popover, drawer, or modal |
| Catalogue | `/` | Move focus to catalogue search |
| Catalogue | `Shift + F` | Toggle advanced filters |
| Viewer | `Space` | Play/pause the working animation |
| Viewer | `ArrowLeft` / `ArrowRight` | Step the animation backward/forward |
| Viewer | `-` / `=` | Decrease/increase animation speed |
| Viewer | `E` | Toggle exploded view |
| Viewer | `[` / `]` | Decrease/increase exploded separation |
| Viewer | `S` / `W` / `X` | Switch solid, wireframe, or cross-section mode |
| Viewer | `R` | Reset camera |
| Viewer | `F` | Frame selected component or full machine |
| Viewer | `1` / `2` / `3` / `4` | Isometric, front, side, and top camera presets |
| Viewer | `C` | Toggle component visibility panel |
## Implementation pattern
Use the pure utility for validation, display, and event matching:
```ts
import {
DEFAULT_VIEWER_SHORTCUTS,
findShortcutConflicts,
matchesShortcut,
shortcutToLabel,
} from '../src/utils/keyboardShortcuts';
const conflicts = findShortcutConflicts(DEFAULT_VIEWER_SHORTCUTS);
const label = shortcutToLabel('Shift+/');
```
Use `useKeyboardShortcuts` when a React surface owns a set of actions:
```ts
useKeyboardShortcuts(
DEFAULT_VIEWER_SHORTCUTS.map((definition) => ({
...definition,
handler: () => dispatchViewerAction(definition.id),
})),
{
target: () => canvasRef.current,
onConflict: reportShortcutConflicts,
},
);
```
The hook registers a single `keydown` listener on the supplied target, skips disabled definitions, reports conflicts, respects editable/interactive target rules, and stops after the first matching enabled shortcut.
## Quality gates
- New shortcut definitions must pass `findShortcutConflicts`.
- Help UI must render labels through `shortcutToLabel` so Mac, Windows, and Linux users see platform-appropriate modifiers.
- Viewer shortcuts must be tested with focus on both the canvas and adjacent controls to verify that shortcuts do not break native button/link/form interaction.
- Shortcuts that mutate view state should update the URL state model when the equivalent pointer-driven control would do so.