}>
  Let users redefine key bindings with a settings UI. duck-vim provides `KeyRecorder` (and `useKeyRecorder`) to capture key combinations, and `formatForDisplay` to render them.

***

## React implementation

```tsx
import { useState, useContext } from 'react'
import { KeyContext, useKeyRecorder } from '@gentleduck/vim/react'
import { formatForDisplay } from '@gentleduck/vim/format'
import type { Command } from '@gentleduck/vim/command'

interface ShortcutEntry {
  id: string
  name: string
  binding: string
  command: Command
}

function ShortcutSettings() {
  const ctx = useContext(KeyContext)
  const { state, start, stop, reset } = useKeyRecorder()
  const [editingId, setEditingId] = useState<string | null>(null)
  const [shortcuts, setShortcuts] = useState<ShortcutEntry[]>(() => {
    if (!ctx) return []
    return Array.from(ctx.registry.getAllCommands()).map(([binding, cmd]) => ({
      id: binding,
      name: cmd.name,
      binding,
      command: cmd,
    }))
  })

  function startEditing(id: string) {
    setEditingId(id)
    reset()
    start()
  }

  function saveBinding(id: string) {
    if (!state.recorded || !ctx) return

    stop()

    const entry = shortcuts.find((s) => s.id === id)
    if (!entry) return

    // Unregister old binding
    ctx.registry.unregister(entry.binding)

    // Register new binding
    ctx.registry.register(state.recorded, entry.command)

    // Update local state
    setShortcuts((prev) =>
      prev.map((s) => (s.id === id ? { ...s, binding: state.recorded! } : s)),
    )

    setEditingId(null)
    reset()
  }

  function cancelEditing() {
    stop()
    setEditingId(null)
    reset()
  }

  return (
    <div className="space-y-2">
      <h2 className="text-lg font-semibold">Keyboard Shortcuts</h2>
      {shortcuts.map((shortcut) => (
        <div key={shortcut.id} className="flex items-center justify-between py-2 border-b">
          <span>{shortcut.name}</span>

          {editingId === shortcut.id ? (
            <div className="flex items-center gap-2">
              <kbd className="px-2 py-1 bg-blue-100 rounded text-sm">
                {state.recorded ?? 'Press a key...'}
              </kbd>
              <button onClick={() => saveBinding(shortcut.id)}>Save</button>
              <button onClick={cancelEditing}>Cancel</button>
            </div>
          ) : (
            <button
              onClick={() => startEditing(shortcut.id)}
              className="px-2 py-1 bg-gray-100 rounded text-sm hover:bg-gray-200"
            >
              {formatForDisplay(shortcut.binding)}
            </button>
          )}
        </div>
      ))}
    </div>
  )
}
```

***

## Persisting custom bindings

}>
  duck-vim doesn't include persistence. Store user bindings wherever makes sense for your app (localStorage, database, etc.) and re-register them on startup.

```ts
// Load saved bindings
const saved = JSON.parse(localStorage.getItem('shortcuts') ?? '{}')

// Register with overrides
for (const [defaultBinding, command] of defaultCommands) {
  const userBinding = saved[command.name] ?? defaultBinding
  registry.register(userBinding, command)
}

// Save when changed
function saveBinding(commandName: string, newBinding: string) {
  const saved = JSON.parse(localStorage.getItem('shortcuts') ?? '{}')
  saved[commandName] = newBinding
  localStorage.setItem('shortcuts', JSON.stringify(saved))
}
```

***

## Conflict detection

}>
  Before saving a new binding, check if it conflicts with an existing one.

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

function hasConflict(newBinding: string): string | null {
  const normalized = normalizeKeyBind(newBinding)
  const existing = registry.getAllCommands()

  for (const [binding, cmd] of existing) {
    if (normalizeKeyBind(binding) === normalized) {
      return cmd.name // Return the conflicting command's name
    }
  }

  return null
}
```

***

## Validation

Use `validateKeyBind` to check user input before accepting it:

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

const result = validateKeyBind(state.recorded)
if (!result.valid) {
  showError(result.errors.join(', '))
  return
}
if (result.warnings.length > 0) {
  showWarning(result.warnings.join(', '))
}
```