{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "keyboard-shortcuts",
  "title": "Keyboard Shortcuts",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A searchable shortcut cheat sheet, inline or in a dialog opened by pressing ?, with Command or Ctrl keys chosen for the viewer's platform and spoken key names.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/dialog.json",
    "https://ui.ballmac.com/r/kbd.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/keyboard-shortcuts.tsx",
      "content": "// Ballmac UI: Keyboard Shortcuts. https://ui.ballmac.com/components/keyboard-shortcuts\n\"use client\"\n\nimport * as React from \"react\"\nimport { Keyboard, Search } from \"lucide-react\"\n\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"@/components/ballmac/dialog\"\nimport { Kbd, KbdGroup } from \"@/components/ballmac/kbd\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype ShortcutPlatform = \"mac\" | \"other\"\n\ntype Shortcut = {\n  /** What the shortcut does. */\n  label: string\n  /**\n   * The keys. Use \"Mod\" for Command on Mac and Ctrl elsewhere, plus \"Alt\", \"Shift\", \"Enter\", \"Esc\",\n   * \"Up\", \"Down\", \"Left\", \"Right\", \"Backspace\", \"Tab\" and \"Space\". Anything else is shown as typed.\n   */\n  keys: string[]\n  /** Keys are pressed one after another (\"G\" then \"I\") instead of together. */\n  sequence?: boolean\n  /** A second line of detail. */\n  description?: string\n}\n\ntype ShortcutGroup = {\n  /** Group heading, such as \"Navigation\". */\n  title: string\n  shortcuts: Shortcut[]\n}\n\nconst MAC: Record<string, string> = { Mod: \"⌘\", Alt: \"⌥\", Shift: \"⇧\", Ctrl: \"⌃\", Enter: \"↵\", Esc: \"Esc\", Up: \"↑\", Down: \"↓\", Left: \"←\", Right: \"→\", Backspace: \"⌫\", Tab: \"⇥\", Space: \"Space\" }\nconst OTHER: Record<string, string> = { ...MAC, Mod: \"Ctrl\", Alt: \"Alt\", Shift: \"Shift\", Ctrl: \"Ctrl\", Enter: \"Enter\", Backspace: \"Backspace\", Tab: \"Tab\" }\nconst SPOKEN: Record<string, string> = { \"⌘\": \"Command\", \"⌥\": \"Option\", \"⇧\": \"Shift\", \"⌃\": \"Control\", \"↵\": \"Enter\", \"↑\": \"Up arrow\", \"↓\": \"Down arrow\", \"←\": \"Left arrow\", \"→\": \"Right arrow\", \"⌫\": \"Backspace\", \"⇥\": \"Tab\" }\n\nconst noop = () => () => {}\n\nfunction usePlatform(platform: ShortcutPlatform | \"auto\"): ShortcutPlatform {\n  // The server cannot know the platform, so it renders the non-Mac keys and corrects after hydration.\n  const detected = React.useSyncExternalStore(\n    noop,\n    () => (/Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent) ? \"mac\" : \"other\"),\n    () => \"other\" as ShortcutPlatform\n  )\n  return platform === \"auto\" ? detected : platform\n}\n\nfunction label(key: string, platform: ShortcutPlatform) {\n  return (platform === \"mac\" ? MAC : OTHER)[key] ?? (key.length === 1 ? key.toUpperCase() : key)\n}\n\nfunction ShortcutKeys({ shortcut, platform }: { shortcut: Shortcut; platform: ShortcutPlatform }) {\n  const shown = shortcut.keys.map((k) => label(k, platform))\n  const spoken = shown.map((k) => SPOKEN[k] ?? k).join(shortcut.sequence ? \" then \" : \" plus \")\n  return (\n    <span className=\"flex shrink-0 items-center gap-1.5\">\n      <span className=\"sr-only\">{spoken}</span>\n      <span aria-hidden=\"true\" className=\"flex items-center gap-1.5\">\n        {shortcut.sequence ? (\n          shown.map((k, i) => (\n            <React.Fragment key={i}>\n              {i > 0 && <span className=\"text-[11px] text-muted-foreground\">then</span>}\n              <Kbd>{k}</Kbd>\n            </React.Fragment>\n          ))\n        ) : (\n          <KbdGroup>\n            {shown.map((k, i) => (\n              <Kbd key={i}>{k}</Kbd>\n            ))}\n          </KbdGroup>\n        )}\n      </span>\n    </span>\n  )\n}\n\ntype KeyboardShortcutsProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Shortcuts, grouped. */\n  groups: ShortcutGroup[]\n  /** Show a search box that filters by action or key. */\n  searchable?: boolean\n  /** Which key labels to draw. \"auto\" picks Mac or other after hydration. */\n  platform?: ShortcutPlatform | \"auto\"\n  /** Placeholder of the search box. */\n  searchPlaceholder?: string\n  /** Number of columns on wide containers. */\n  columns?: 1 | 2\n}\n\nfunction KeyboardShortcuts({\n  groups,\n  searchable = true,\n  platform: platformProp = \"auto\",\n  searchPlaceholder,\n  columns = 2,\n  className,\n  ...props\n}: KeyboardShortcutsProps) {\n  const msg = useMessages()\n  searchPlaceholder ??= msg(\"keyboard-shortcuts.searchPlaceholder\", \"Search shortcuts\")\n  const platform = usePlatform(platformProp)\n  const [query, setQuery] = React.useState(\"\")\n  const q = query.trim().toLowerCase()\n  const filtered = groups\n    .map((g) => ({\n      ...g,\n      shortcuts: g.shortcuts.filter(\n        (s) =>\n          !q ||\n          s.label.toLowerCase().includes(q) ||\n          s.description?.toLowerCase().includes(q) ||\n          s.keys.some((k) => label(k, platform).toLowerCase().includes(q) || k.toLowerCase().includes(q))\n      ),\n    }))\n    .filter((g) => g.shortcuts.length > 0)\n  const count = filtered.reduce((n, g) => n + g.shortcuts.length, 0)\n\n  return (\n    <div data-slot=\"keyboard-shortcuts\" className={cn(\"@container w-full\", className)} {...props}>\n      {searchable && (\n        <div className=\"relative mb-4\">\n          <Search aria-hidden=\"true\" className=\"pointer-events-none absolute top-1/2 start-3 size-4 -translate-y-1/2 text-muted-foreground\" />\n          <input\n            type=\"search\"\n            value={query}\n            onChange={(e) => setQuery(e.target.value)}\n            aria-label={searchPlaceholder}\n            placeholder={searchPlaceholder}\n            className=\"h-10 w-full rounded-lg border bg-background pe-3 ps-9 text-sm outline-none transition-[border-color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n          />\n          <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n            {q ? `${count} ${count === 1 ? \"shortcut\" : \"shortcuts\"} found` : \"\"}\n          </span>\n        </div>\n      )}\n      {filtered.length === 0 ? (\n        <p className=\"rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground\">\n          {msg(\"keyboard-shortcuts.noShortcutMatches\", \"No shortcut matches “{query}”.\", { query })}\n        </p>\n      ) : (\n        <div className={cn(\"grid gap-x-10 gap-y-6\", columns === 2 && \"@xl:grid-cols-2\")}>\n          {filtered.map((group) => (\n            <section key={group.title} aria-label={group.title} className=\"min-w-0\">\n              <h3 className=\"mb-1 text-xs font-semibold tracking-wide text-muted-foreground uppercase\">{group.title}</h3>\n              <ul className=\"divide-y\">\n                {group.shortcuts.map((s) => (\n                  <li key={s.label} className=\"flex min-h-10 items-center justify-between gap-4 py-1.5\">\n                    <span className=\"min-w-0\">\n                      <span className=\"block text-sm text-foreground\">{s.label}</span>\n                      {s.description && <span className=\"block text-xs text-muted-foreground\">{s.description}</span>}\n                    </span>\n                    <ShortcutKeys shortcut={s} platform={platform} />\n                  </li>\n                ))}\n              </ul>\n            </section>\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n\ntype KeyboardShortcutsDialogProps = Omit<KeyboardShortcutsProps, \"className\"> & {\n  /** Controlled open state. */\n  open?: boolean\n  /** Initial open state. */\n  defaultOpen?: boolean\n  /** Called when the dialog opens or closes. */\n  onOpenChange?: (open: boolean) => void\n  /** Key that opens the dialog from anywhere outside a text field. Set to `null` to turn it off. */\n  hotkey?: string | null\n  /** Dialog heading. */\n  title?: string\n  /** Line under the heading. */\n  description?: string\n  /** Classes for the dialog panel. */\n  className?: string\n}\n\n/** The cheat sheet in a dialog, opened by pressing \"?\" (or another `hotkey`) anywhere on the page. */\nfunction KeyboardShortcutsDialog({\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  hotkey = \"?\",\n  title,\n  description,\n  className,\n  ...props\n}: KeyboardShortcutsDialogProps) {\n  const msg = useMessages()\n  title ??= msg(\"keyboard-shortcuts.title\", \"Keyboard shortcuts\")\n  description ??= msg(\"keyboard-shortcuts.description\", \"Work faster without leaving the keyboard.\")\n  const [internal, setInternal] = React.useState(defaultOpen)\n  const open = openProp ?? internal\n  const onOpenChangeRef = React.useRef(onOpenChange)\n  React.useEffect(() => {\n    onOpenChangeRef.current = onOpenChange\n  })\n  const setOpen = React.useCallback((next: boolean) => {\n    setInternal(next)\n    onOpenChangeRef.current?.(next)\n  }, [])\n\n  React.useEffect(() => {\n    if (!hotkey) return\n    function onKey(event: KeyboardEvent) {\n      if (event.key !== hotkey || event.metaKey || event.ctrlKey || event.altKey) return\n      const el = event.target as HTMLElement | null\n      if (el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName))) return\n      event.preventDefault()\n      setOpen(!open)\n    }\n    window.addEventListener(\"keydown\", onKey)\n    return () => window.removeEventListener(\"keydown\", onKey)\n  }, [hotkey, open, setOpen])\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogContent className={cn(\"max-h-[85dvh] grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden p-0 sm:max-w-3xl sm:p-0\", className)}>\n        <DialogHeader className=\"border-b px-5 py-4 pe-12\">\n          <DialogTitle className=\"flex items-center gap-2\">\n            <Keyboard aria-hidden=\"true\" className=\"size-4 text-muted-foreground\" />\n            {title}\n          </DialogTitle>\n          <DialogDescription>{description}</DialogDescription>\n        </DialogHeader>\n        <div className=\"overflow-y-auto p-5\">\n          <KeyboardShortcuts {...props} />\n        </div>\n        {hotkey && (\n          <p className=\"border-t bg-muted/40 px-5 py-2.5 text-xs text-muted-foreground\">\n            {msg.rich(\"keyboard-shortcuts.press\", \"Press {key} anywhere to open this list.\", { key: <Kbd size=\"sm\">{hotkey}</Kbd> })}\n          </p>\n        )}\n      </DialogContent>\n    </Dialog>\n  )\n}\n\nexport {\n  KeyboardShortcuts,\n  KeyboardShortcutsDialog,\n  type KeyboardShortcutsProps,\n  type KeyboardShortcutsDialogProps,\n  type Shortcut,\n  type ShortcutGroup,\n  type ShortcutPlatform,\n}\n",
      "type": "registry:ui",
      "target": "@components/ballmac/keyboard-shortcuts.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "keyboard",
      "shortcuts",
      "hotkeys",
      "help",
      "dialog"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "groups is [{ title, shortcuts: [{ label, keys, sequence?, description? }] }]. Use 'Mod' for Cmd on Mac and Ctrl elsewhere. <KeyboardShortcutsDialog hotkey='?'> opens from anywhere outside a text field. platform='auto' corrects after hydration.",
      "whenToUse": [
        "A help overlay for apps with many shortcuts",
        "Docs pages that list hotkeys"
      ],
      "whenNotToUse": [
        "A single hint beside a button (shortcut-hint)",
        "Binding the shortcuts themselves; this only displays them"
      ],
      "composesWith": [
        "kbd",
        "shortcut-hint",
        "command-bar",
        "dialog"
      ],
      "a11y": [
        {
          "keys": "?",
          "action": "Opens the dialog when focus is not in a text field"
        },
        {
          "keys": "Escape",
          "action": "Closes the dialog"
        },
        {
          "keys": "Screen readers",
          "action": "Keys read as 'Command plus K', sequences as 'G then I'; the filtered count is announced"
        }
      ],
      "customization": [
        "platform",
        "columns",
        "searchable",
        "hotkey (or null)",
        "sequence shortcuts"
      ]
    },
    "examples": [
      "keyboard-shortcuts-demo",
      "keyboard-shortcuts-dialog"
    ],
    "url": "https://ui.ballmac.com/components/keyboard-shortcuts"
  },
  "categories": [
    "developer"
  ],
  "type": "registry:ui"
}