{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "keyboard-shortcuts",
  "type": "registry:ui",
  "title": "Keyboard shortcuts",
  "description": "A keyboard shortcuts cheat sheet in a dialog, opened with the ? key or a trigger button, with search and the right modifier glyphs for the viewer's platform.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://ui.minidev.pro/r/button.json",
    "https://ui.minidev.pro/r/dialog.json",
    "https://ui.minidev.pro/r/input.json",
    "https://ui.minidev.pro/r/kbd.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/keyboard-shortcuts.tsx",
      "type": "registry:ui",
      "target": "components/ui/keyboard-shortcuts.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { KeyboardIcon, SearchIcon } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"@/components/ui/dialog\"\nimport { Input } from \"@/components/ui/input\"\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\"\n\n/**\n * A keyboard shortcuts cheat sheet in a dialog, opened with the ? key or a trigger button, with search and the right modifier glyphs for the viewer's platform.\n * Shortcuts are grouped, filtered as you type, and written once with\n * portable names (mod, shift, alt, enter) that render as ⌘ ⇧ ⌥ ↵ on a Mac\n * and Ctrl, Shift, Alt, Enter elsewhere. The platform is read after mount,\n * so server and client markup match.\n */\ntype KeyboardShortcut = {\n  /** Keys pressed together. Use mod for Command or Control, plus shift, alt, ctrl, enter, esc, tab, backspace, space, up, down, left, right. */\n  keys: string[]\n  label: string\n}\n\ntype KeyboardShortcutGroup = {\n  title: string\n  shortcuts: KeyboardShortcut[]\n}\n\ntype KeyboardShortcutsProps = {\n  groups?: KeyboardShortcutGroup[]\n  title?: string\n  /** Key that opens the sheet from anywhere on the page, outside text fields. Empty disables it. */\n  hotkey?: string\n  /** Controlled open state. Leave undefined for internal state. */\n  open?: boolean\n  onOpenChange?: (open: boolean) => void\n  /** Render the built-in trigger button. */\n  showTrigger?: boolean\n  triggerLabel?: string\n  className?: string\n}\n\nconst SAMPLE_GROUPS: KeyboardShortcutGroup[] = [\n  {\n    title: \"Navigation\",\n    shortcuts: [\n      { keys: [\"mod\", \"K\"], label: \"Command palette\" },\n      { keys: [\"G\", \"O\"], label: \"Go to overview\" },\n      { keys: [\"G\", \"I\"], label: \"Go to invoices\" },\n      { keys: [\"G\", \"C\"], label: \"Go to customers\" },\n      { keys: [\"/\"], label: \"Search\" },\n    ],\n  },\n  {\n    title: \"Invoices\",\n    shortcuts: [\n      { keys: [\"N\"], label: \"New invoice\" },\n      { keys: [\"mod\", \"enter\"], label: \"Send invoice\" },\n      { keys: [\"mod\", \"shift\", \"P\"], label: \"Mark as paid\" },\n      { keys: [\"mod\", \"D\"], label: \"Duplicate\" },\n      { keys: [\"mod\", \"backspace\"], label: \"Archive\" },\n    ],\n  },\n  {\n    title: \"Editor\",\n    shortcuts: [\n      { keys: [\"mod\", \"B\"], label: \"Bold\" },\n      { keys: [\"mod\", \"I\"], label: \"Italic\" },\n      { keys: [\"mod\", \"Z\"], label: \"Undo\" },\n      { keys: [\"mod\", \"shift\", \"Z\"], label: \"Redo\" },\n      { keys: [\"mod\", \"S\"], label: \"Save draft\" },\n    ],\n  },\n]\n\nconst MAC: Record<string, string> = { mod: \"⌘\", shift: \"⇧\", alt: \"⌥\", ctrl: \"⌃\", enter: \"↵\", backspace: \"⌫\", esc: \"Esc\", tab: \"⇥\", space: \"Space\", up: \"↑\", down: \"↓\", left: \"←\", right: \"→\" }\nconst OTHER: Record<string, string> = { mod: \"Ctrl\", shift: \"Shift\", alt: \"Alt\", ctrl: \"Ctrl\", enter: \"Enter\", backspace: \"Backspace\", esc: \"Esc\", tab: \"Tab\", space: \"Space\", up: \"↑\", down: \"↓\", left: \"←\", right: \"→\" }\n/** Words a search should also match, so \"command\" finds ⌘ shortcuts on a Mac. */\nconst ALIASES: Record<string, string> = { mod: \"command cmd control ctrl\", shift: \"shift\", alt: \"alt option\", ctrl: \"control ctrl\", enter: \"enter return\", backspace: \"backspace delete\", esc: \"escape\", tab: \"tab\" }\n\nfunction keyLabel(key: string, mac: boolean) {\n  const k = key.toLowerCase()\n  return (mac ? MAC : OTHER)[k] ?? key\n}\n\nfunction isTypingTarget(el: EventTarget | null) {\n  if (!(el instanceof HTMLElement)) return false\n  return el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)\n}\n\nfunction KeyboardShortcuts({\n  groups = SAMPLE_GROUPS,\n  title = \"Keyboard shortcuts\",\n  hotkey = \"?\",\n  open,\n  onOpenChange,\n  showTrigger = true,\n  triggerLabel = \"Keyboard shortcuts\",\n  className,\n}: KeyboardShortcutsProps) {\n  const id = React.useId()\n  const [inner, setInner] = React.useState(false)\n  const [query, setQuery] = React.useState(\"\")\n  const [mac, setMac] = React.useState(false)\n  const isOpen = open ?? inner\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (open === undefined) setInner(next)\n      onOpenChange?.(next)\n    },\n    [open, onOpenChange]\n  )\n\n  React.useEffect(() => {\n    const nav = navigator as Navigator & { userAgentData?: { platform?: string } }\n    const platform = nav.userAgentData?.platform ?? nav.platform ?? nav.userAgent\n    setMac(/mac|iphone|ipad|ipod/i.test(platform))\n  }, [])\n\n  React.useEffect(() => {\n    if (!hotkey) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.metaKey || e.ctrlKey || e.altKey) return\n      // Some layouts report Shift+/ with key \"/\" instead of \"?\".\n      const hit = e.key === hotkey || (hotkey === \"?\" && e.key === \"/\" && e.shiftKey)\n      if (!hit || isTypingTarget(e.target)) return\n      e.preventDefault()\n      setOpen(true)\n    }\n    document.addEventListener(\"keydown\", onKey)\n    return () => document.removeEventListener(\"keydown\", onKey)\n  }, [hotkey, setOpen])\n\n  React.useEffect(() => {\n    if (!isOpen) setQuery(\"\")\n  }, [isOpen])\n\n  const q = query.trim().toLowerCase()\n  const visible = groups\n    .map((g) => ({\n      ...g,\n      shortcuts: q\n        ? g.shortcuts.filter((s) => {\n            const hay = [s.label, g.title, ...s.keys.flatMap((k) => [k, keyLabel(k, mac), ALIASES[k.toLowerCase()] ?? \"\"])].join(\" \").toLowerCase()\n            return hay.includes(q)\n          })\n        : g.shortcuts,\n    }))\n    .filter((g) => g.shortcuts.length)\n  const count = visible.reduce((n, g) => n + g.shortcuts.length, 0)\n\n  return (\n    <div data-slot=\"keyboard-shortcuts\" className={cn(\"inline-flex\", className)}>\n      {showTrigger ? (\n        <Button type=\"button\" variant=\"outline\" onClick={() => setOpen(true)} aria-haspopup=\"dialog\" aria-expanded={isOpen} data-slot=\"keyboard-shortcuts-trigger\">\n          <KeyboardIcon />\n          {triggerLabel}\n          {hotkey ? <Kbd className=\"ml-1\">{hotkey}</Kbd> : null}\n        </Button>\n      ) : null}\n      <Dialog open={isOpen} onOpenChange={setOpen}>\n        <DialogContent className=\"gap-0 p-0 sm:max-w-xl\" data-slot=\"keyboard-shortcuts-sheet\">\n          <DialogHeader className=\"border-b border-border px-5 pt-5 pb-4\">\n            <DialogTitle>{title}</DialogTitle>\n            <DialogDescription>\n              {hotkey ? (\n                <>\n                  Press <Kbd>{hotkey}</Kbd> anywhere to open this sheet.\n                </>\n              ) : (\n                \"Everything you can do without leaving the keyboard.\"\n              )}\n            </DialogDescription>\n            <div className=\"relative mt-2\">\n              <SearchIcon aria-hidden className=\"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-fg-subtle\" />\n              <Input\n                autoFocus\n                type=\"search\"\n                value={query}\n                onChange={(e) => setQuery(e.target.value)}\n                placeholder=\"Search shortcuts\"\n                aria-label=\"Search shortcuts\"\n                data-slot=\"keyboard-shortcuts-search\"\n                className=\"pl-9\"\n              />\n            </div>\n          </DialogHeader>\n          <div data-slot=\"keyboard-shortcuts-body\" className=\"max-h-[min(60vh,480px)] overflow-y-auto px-5 py-4\">\n            {count ? (\n              <div className=\"grid gap-x-8 gap-y-5 sm:grid-cols-2\">\n                {visible.map((g, gi) => (\n                  <section key={g.title} data-slot=\"keyboard-shortcuts-group\" aria-labelledby={`${id}-g${gi}`} className=\"min-w-0\">\n                    <h3 id={`${id}-g${gi}`} className=\"mb-1 text-[11px] font-medium tracking-[0.02em] text-fg-subtle uppercase\">\n                      {g.title}\n                    </h3>\n                    <ul className=\"divide-y divide-border\">\n                      {g.shortcuts.map((s) => (\n                        <li key={s.label} className=\"flex h-9 items-center justify-between gap-4\">\n                          <span className=\"truncate text-sm text-fg\">{s.label}</span>\n                          <KbdGroup aria-label={s.keys.map((k) => keyLabel(k, mac)).join(\" \")}>\n                            {s.keys.map((k, i) => (\n                              <Kbd key={`${k}-${i}`} aria-hidden>\n                                {keyLabel(k, mac)}\n                              </Kbd>\n                            ))}\n                          </KbdGroup>\n                        </li>\n                      ))}\n                    </ul>\n                  </section>\n                ))}\n              </div>\n            ) : (\n              <p className=\"py-8 text-center text-sm text-fg-muted\">No shortcuts match “{query.trim()}”.</p>\n            )}\n          </div>\n          <div className=\"flex items-center justify-between rounded-b-2xl border-t border-border bg-sunken px-5 py-2.5 text-xs text-fg-muted\">\n            <span>\n              {count} {count === 1 ? \"shortcut\" : \"shortcuts\"}\n            </span>\n            <span className=\"inline-flex items-center gap-1.5\">\n              <Kbd>Esc</Kbd> to close\n            </span>\n          </div>\n        </DialogContent>\n      </Dialog>\n    </div>\n  )\n}\n\nexport { KeyboardShortcuts }\nexport type { KeyboardShortcutsProps, KeyboardShortcut, KeyboardShortcutGroup }\n"
    }
  ]
}
