}>
  Lesson 8 of 8  -  Scoped bindings, command palettes, conflict detection, testing, and production best practices.

## Scoped bindings

Not every shortcut should be global. Some should only work when a specific element is focused.

Vanilla
React

```ts
const editorRegistry = new Registry()
const editorHandler = new KeyHandler(editorRegistry, 600)

editorRegistry.register('ctrl+s', saveCommand, { preventDefault: true })
editorRegistry.register('ctrl+z', undoCommand, { preventDefault: true })

const editorEl = document.getElementById('editor')!
editorHandler.attach(editorEl)
```

```tsx
function Editor() {
  const ref = useRef<HTMLDivElement>(null)

  useKeyBind('ctrl+s', () => save(), {
    preventDefault: true,
    targetRef: ref,
  })

  return <div ref={ref} tabIndex={0}>Editor</div>
}
```

}>
  Remember: non-natively-focusable elements need `tabIndex={0}` to receive keyboard events.
  
  

### Nested scopes

Use `stopPropagation: true` to prevent shortcuts from bubbling up:

```tsx
// Modal catches Escape first
useKeyBind('escape', () => closeModal(), {
  targetRef: modalRef,
  stopPropagation: true,
})

// App-level Escape won't fire when modal is open
useKeyBind('escape', () => closeSidebar())
```

***

## Building a command palette

}>
  Since the Registry stores names and descriptions, building a command palette is straightforward.

```tsx
function CommandPalette() {
  const ctx = useContext(KeyContext)
  const [query, setQuery] = useState('')

  const items = useMemo(() => {
    if (!ctx) return []
    const all = Array.from(ctx.registry.getAllCommands())
    if (!query) return all
    const q = query.toLowerCase()
    return all.filter(([, cmd]) => cmd.name.toLowerCase().includes(q))
  }, [ctx, query])

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search commands..."
        autoFocus
      />
      {items.map(([binding, cmd]) => (
        <button key={binding} onClick={() => cmd.execute()}>
          {cmd.name}
          <kbd>{formatForDisplay(binding)}</kbd>
        </button>
      ))}
    </div>
  )
}
```

See the [Command Palette guide](/duck-vim/guides/command-palette) for a more complete implementation.

***

## Conflict detection

}>
  When users customize shortcuts or when you register many bindings, conflicts can occur. Detect them early.

```ts
import { normalizeKeyBind } from '@gentleduck/vim/parser'

function findConflicts(registry: Registry): Array<[string, string]> {
  const normalized = new Map<string, string[]>()

  for (const [binding] of registry.getAllCommands()) {
    const norm = normalizeKeyBind(binding)
    const existing = normalized.get(norm) ?? []
    existing.push(binding)
    normalized.set(norm, existing)
  }

  const conflicts: Array<[string, string]> = []
  for (const [, bindings] of normalized) {
    if (bindings.length > 1) {
      conflicts.push([bindings[0]!, bindings[1]!])
    }
  }

  return conflicts
}
```

Or use the `conflictBehavior` option during registration:

```ts
registry.register('ctrl+k', command, { conflictBehavior: 'error' })
// Throws if ctrl+k is already registered
```

***

## The `requireReset` pattern

}>
  For one-shot actions like "Save", you might want the shortcut to fire only once until explicitly reset (to prevent double-saves from held keys).

```ts
const handle = registry.register('ctrl+s', {
  name: 'Save',
  execute: async () => {
    await saveDocument()
    handle.resetFired() // Allow firing again
  },
}, { requireReset: true, preventDefault: true })
```

Without `requireReset`, holding <Kbd>Ctrl+S</Kbd> would trigger `save()` on every `keydown` repeat event.

***

## Testing

### Unit testing key parsing

```ts
import { parseKeyBind, normalizeKeyBind, validateKeyBind } from '@gentleduck/vim/parser'

test('parses Mod+S on Mac', () => {
  const result = parseKeyBind('Mod+S', 'mac')
  expect(result.meta).toBe(true)
  expect(result.ctrl).toBe(false)
  expect(result.key).toBe('s')
})

test('normalizes binding order', () => {
  expect(normalizeKeyBind('Shift+Ctrl+K')).toBe('ctrl+shift+k')
})

test('validates empty binding', () => {
  const result = validateKeyBind('')
  expect(result.valid).toBe(false)
})
```

### Unit testing matching

```ts
import { matchesKeyboardEvent } from '@gentleduck/vim/matcher'
import { parseKeyBind } from '@gentleduck/vim/parser'

test('matches ctrl+k event', () => {
  const parsed = parseKeyBind('ctrl+k')
  const event = new KeyboardEvent('keydown', {
    key: 'k',
    ctrlKey: true,
  })
  expect(matchesKeyboardEvent(parsed, event)).toBe(true)
})

test('does not match without ctrl', () => {
  const parsed = parseKeyBind('ctrl+k')
  const event = new KeyboardEvent('keydown', { key: 'k' })
  expect(matchesKeyboardEvent(parsed, event)).toBe(false)
})
```

### Integration testing the Registry

```ts
import { Registry, KeyHandler } from '@gentleduck/vim/command'

test('executes command on key match', () => {
  const registry = new Registry()
  const handler = new KeyHandler(registry)
  const fn = vi.fn()

  registry.register('ctrl+k', { name: 'test', execute: fn })
  handler.attach(document)

  document.dispatchEvent(
    new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true }),
  )

  expect(fn).toHaveBeenCalledOnce()
  handler.detach(document)
})
```

### Testing platform detection

```ts
import { _resetPlatformCache } from '@gentleduck/vim/platform'

beforeEach(() => {
  _resetPlatformCache()
})
```

***

## Production best practices

### Centralize registrations

Define all shortcuts in one place for discoverability:

```ts
// shortcuts.ts
export const APP_SHORTCUTS = {
  'Mod+k': { name: 'Command Palette', execute: openPalette },
  'Mod+s': { name: 'Save', execute: save },
  'g+d': { name: 'Go Dashboard', execute: () => navigate('/dashboard') },
  'g+s': { name: 'Go Settings', execute: () => navigate('/settings') },
} as const
```

### Use `ignoreInputs` for character bindings

}>
  Any binding that doesn't use a modifier (like <Kbd>G</Kbd>, <Kbd>D</Kbd>, <Kbd>/</Kbd>) should have `ignoreInputs: true`. Otherwise it fires when the user is typing.
  
  

  
  ### Always `preventDefault` for browser shortcuts

  <Kbd>Ctrl+S</Kbd>, <Kbd>Ctrl+W</Kbd>, <Kbd>Ctrl+N</Kbd>, <Kbd>Ctrl+P</Kbd>, <Kbd>Ctrl+T</Kbd> all have browser defaults. Set `preventDefault: true` or the browser action fires alongside your command.

### Debug in development only

```ts
const registry = new Registry(process.env.NODE_ENV === 'development')
```

### Clean up on unmount

Every `register()` call should have a corresponding `unregister()`. The React hooks handle this automatically. In vanilla code, track your handles.

### Test cross-platform

Use the `platform` parameter in `parseKeyBind` and `formatForDisplay` to test all three platforms without needing different machines:

```ts
for (const platform of ['mac', 'windows', 'linux'] as const) {
  const result = parseKeyBind('Mod+S', platform)
  console.log(platform, result)
}
```

***

## What you've learned

}>
  Eight lessons covering the full surface for building a keyboard-driven interface.

1. Why keyboard shortcuts matter and how duck-vim's registry pattern solves common problems.
2. How to create a Registry, register commands, and attach a KeyHandler.
3. How key binding strings are parsed, normalized, and validated.
4. How to integrate with React using KeyProvider and hooks.
5. How multi-key sequences work with both the Registry and SequenceManager.
6. How to format shortcuts for display with platform-aware labels.
7. How to build a shortcut customization UI with the KeyRecorder.
8. Advanced patterns: scoping, command palettes, conflict detection, and testing.

From here, ship it.