{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-grid-keybindings",
  "title": "Data Grid Keybindings",
  "author": "gridcn",
  "description": "Keyboard shortcuts dialog for the gridcn data grid, generated at runtime from the grid's effective keymap, grouped by category with platform-aware key chips; opens via '?' or a trigger.",
  "dependencies": [],
  "registryDependencies": [
    "DammersCode/gridcn/data-grid",
    "dialog"
  ],
  "files": [
    {
      "path": "registry/default/blocks/data-grid-keybindings/data-grid-keybindings.ts",
      "content": "/** Public entry point — the only module consumers/docs should import from. */\r\n\r\nexport { DataGridKeybindingsDialog, type DataGridKeybindingsDialogProps } from \"./keybindings-dialog\";\r\nexport { DataGridKeybindingsShortcut, type DataGridKeybindingsShortcutProps } from \"./keybindings-shortcut\";\r\nexport { BindingChips, bindingTokens } from \"./binding-label\";\r\nexport { KEYBINDING_CATEGORIES, ACTION_CATEGORY, categoryForAction, categoryLabel, type KeybindingCategory } from \"./action-groups\";\r\nexport { labelForAction } from \"./action-labels\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid-keybindings/data-grid-keybindings.ts"
    },
    {
      "path": "registry/default/blocks/data-grid-keybindings/keybindings-dialog.tsx",
      "content": "\"use client\";\r\n\r\nimport { useMemo, type ReactElement, type ReactNode } from \"react\";\r\nimport {\r\n  Dialog,\r\n  DialogContent,\r\n  DialogHeader,\r\n  DialogTitle,\r\n  DialogDescription,\r\n  DialogTrigger,\r\n} from \"@/components/ui/dialog\";\r\nimport { useDataGridKeymap, useDataGridLabels, type DataGridLabels, type GridAction, type Keymap } from \"@/registry/default/components/data-grid/data-grid\";\r\nimport { KEYBINDING_CATEGORIES, categoryForAction, categoryLabel, type KeybindingCategory } from \"./action-groups\";\r\nimport { labelForAction } from \"./action-labels\";\r\nimport { BindingChips } from \"./binding-label\";\r\n\r\n/** One always-on native clipboard shortcut not covered by the keymap (copy/cut/paste are native browser events, see use-grid-clipboard.ts); labels come from `labels.keybindings.native*` (i18n). */\r\nfunction nativeClipboardBindings(labels: DataGridLabels): { label: string; binding: string }[] {\r\n  return [\r\n    { label: labels.keybindings.nativeCopy, binding: \"mod+c\" },\r\n    { label: labels.keybindings.nativeCut, binding: \"mod+x\" },\r\n    { label: labels.keybindings.nativePaste, binding: \"mod+v\" },\r\n  ];\r\n}\r\n\r\n/** A dialog row: either a keymap-driven `GridAction` (key = action name) or a native binding (key = its label). */\r\ntype KeymapRow = { key: string; label: string; bindings: string[] };\r\n\r\n/** Every bound action in `keymap` plus the native clipboard bindings, grouped by category in {@link KEYBINDING_CATEGORIES} order; categories with no rows are omitted. */\r\nfunction groupKeymap(keymap: Keymap, labels: DataGridLabels): { category: KeybindingCategory; rows: KeymapRow[] }[] {\r\n  const byCategory = new Map<KeybindingCategory, KeymapRow[]>();\r\n  const pushRow = (category: KeybindingCategory, row: KeymapRow) => {\r\n    const list = byCategory.get(category) ?? [];\r\n    list.push(row);\r\n    byCategory.set(category, list);\r\n  };\r\n\r\n  for (const [action, bindings] of Object.entries(keymap) as [GridAction, string[] | undefined][]) {\r\n    if (!bindings || bindings.length === 0) continue;\r\n    pushRow(categoryForAction(action), { key: action, label: labelForAction(action, labels), bindings });\r\n  }\r\n  // native clipboard events (copy/cut/paste) aren't GridActions/keymap entries — see use-grid-clipboard.ts\r\n  for (const { label, binding } of nativeClipboardBindings(labels)) {\r\n    pushRow(\"clipboardFill\", { key: label, label, bindings: [binding] });\r\n  }\r\n\r\n  return KEYBINDING_CATEGORIES.map((category) => ({ category, rows: byCategory.get(category) ?? [] })).filter(\r\n    (group) => group.rows.length > 0,\r\n  );\r\n}\r\n\r\n/** Props for {@link DataGridKeybindingsDialog}. */\r\nexport type DataGridKeybindingsDialogProps = {\r\n  /** Controlled open state; omit for uncontrolled (internal) state. */\r\n  open?: boolean;\r\n  onOpenChange?: (open: boolean) => void;\r\n  /** Uncontrolled initial open state; ignored when `open` is provided. */\r\n  defaultOpen?: boolean;\r\n  /** Rendered as the dialog's trigger (e.g. a `<Button>`); omit to drive `open` externally only. */\r\n  trigger?: ReactElement;\r\n};\r\n\r\n/**\r\n * Lists every bound `GridAction` from the mounted grid's EFFECTIVE keymap (`DEFAULT_KEYMAP` merged\r\n * with the consumer's `keymap` prop via `useDataGridKeymap`) — never a hardcoded shortcut list, so\r\n * consumer overrides/extensions show up automatically. Grouped by category, platform-aware `<kbd>`\r\n * chips (⌘ on mac, Ctrl elsewhere; Space/arrow glyphs). Copy/Cut/Paste are appended separately since\r\n * they're native browser clipboard events, not part of the keymap.\r\n */\r\nexport function DataGridKeybindingsDialog(props: DataGridKeybindingsDialogProps): ReactNode {\r\n  const { open, onOpenChange, defaultOpen, trigger } = props;\r\n  const keymap = useDataGridKeymap();\r\n  const labels = useDataGridLabels();\r\n  const groups = useMemo(() => groupKeymap(keymap, labels), [keymap, labels]);\r\n\r\n  return (\r\n    <Dialog open={open} onOpenChange={onOpenChange} defaultOpen={defaultOpen}>\r\n      {trigger && <DialogTrigger render={trigger} />}\r\n      <DialogContent className=\"max-h-[80vh] max-w-lg overflow-y-auto sm:max-w-lg\" data-slot=\"data-grid-keybindings-dialog\">\r\n        <DialogHeader>\r\n          <DialogTitle>{labels.keybindings.title}</DialogTitle>\r\n          <DialogDescription>{labels.keybindings.description}</DialogDescription>\r\n        </DialogHeader>\r\n        <div className=\"flex flex-col gap-4\">\r\n          {groups.map(({ category, rows }) => (\r\n            <section key={category} data-keybindings-category={category}>\r\n              <h3 className=\"mb-1.5 text-xs font-semibold tracking-wide text-muted-foreground uppercase\">{categoryLabel(category, labels)}</h3>\r\n              <ul className=\"flex flex-col gap-1\">\r\n                {rows.map((row) => (\r\n                  <li key={row.key} data-keybindings-action={row.key} className=\"flex items-center justify-between gap-4 py-0.5\">\r\n                    <span className=\"text-foreground\">{row.label}</span>\r\n                    <div className=\"flex items-center gap-2\">\r\n                      {row.bindings.map((binding) => (\r\n                        <BindingChips key={binding} binding={binding} />\r\n                      ))}\r\n                    </div>\r\n                  </li>\r\n                ))}\r\n              </ul>\r\n            </section>\r\n          ))}\r\n        </div>\r\n      </DialogContent>\r\n    </Dialog>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-keybindings/keybindings-dialog.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid-keybindings/keybindings-shortcut.tsx",
      "content": "\"use client\";\r\n\r\nimport { useEffect, type ReactNode } from \"react\";\r\nimport { useDataGridContainer, useDataGridEditing } from \"@/registry/default/components/data-grid/data-grid\";\r\n\r\n/** Props for {@link DataGridKeybindingsShortcut}. */\r\nexport type DataGridKeybindingsShortcutProps = {\r\n  /** Called when the grid container is focused, no editor is open, and the user presses `?` (shift+/). */\r\n  onOpen: () => void;\r\n};\r\n\r\n/**\r\n * Headless: mount INSIDE `DataGridRoot` (it reads the root's container element, so it throws\r\n * outside the root — the dialog itself only needs the provider) to open the keybindings dialog on\r\n * `?` (shift+/) while the grid container has focus and no cell editor is open — mirrors how\r\n * Excel/Notion reserve `?` for a shortcuts overlay only when it can't collide with typing into a\r\n * cell. `?` is a contested key: while this component is mounted and the grid has focus, it claims\r\n * `?` (it stops the native event before the grid's keymap and type-to-edit see it), shadowing\r\n * type-to-edit and any `keymap` binding on `?`.\r\n */\r\nexport function DataGridKeybindingsShortcut(props: DataGridKeybindingsShortcutProps): ReactNode {\r\n  const { onOpen } = props;\r\n  const containerRef = useDataGridContainer();\r\n  const editing = useDataGridEditing();\r\n\r\n  useEffect(() => {\r\n    const container = containerRef.current;\r\n    if (!container || editing) return;\r\n\r\n    function onKeyDown(event: KeyboardEvent) {\r\n      if (event.key !== \"?\" || event.ctrlKey || event.metaKey || event.altKey) return;\r\n      event.preventDefault();\r\n      // stop the native event before it reaches React's root-delegated onKeyDown, else the\r\n      // grid's type-to-edit fallback (no keymap binding for \"?\") also fires and overwrites the cell\r\n      event.stopImmediatePropagation();\r\n      onOpen();\r\n    }\r\n\r\n    container.addEventListener(\"keydown\", onKeyDown);\r\n    return () => container.removeEventListener(\"keydown\", onKeyDown);\r\n  }, [containerRef, editing, onOpen]);\r\n\r\n  return null;\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-keybindings/keybindings-shortcut.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid-keybindings/binding-label.tsx",
      "content": "import type { ReactNode } from \"react\";\r\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\";\r\n\r\n/** True when running on macOS (⌘-labeled shortcuts); false everywhere else (Ctrl-labeled). SSR-safe (false when no `navigator`). */\r\nfunction isMac(): boolean {\r\n  if (typeof navigator === \"undefined\") return false;\r\n  // ponytail: duplicated from core's use-grid-interaction.ts isMacPlatform — deliberate, no add-on may depend on another.\r\n  const uaData = (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData;\r\n  const platform = uaData?.platform ?? navigator.platform ?? navigator.userAgent;\r\n  // case-insensitive: userAgentData.platform reports \"macOS\" (lowercase 'ac'), unlike legacy \"MacIntel\".\r\n  return /mac|iphone|ipad/i.test(platform);\r\n}\r\n\r\nconst ARROW_GLYPHS: Record<string, string> = {\r\n  ArrowUp: \"↑\",\r\n  ArrowDown: \"↓\",\r\n  ArrowLeft: \"←\",\r\n  ArrowRight: \"→\",\r\n};\r\n\r\n/** One binding segment (\"mod\", \"shift\", \"ArrowUp\") to its display token, platform-aware for \"mod\". */\r\nfunction displaySegment(segment: string): string {\r\n  if (segment === \"mod\") return isMac() ? \"⌘\" : \"Ctrl\";\r\n  if (segment === \"ctrl\") return \"Ctrl\";\r\n  if (segment === \"shift\") return \"Shift\";\r\n  if (segment === \"alt\") return isMac() ? \"⌥\" : \"Alt\";\r\n  if (segment === \" \") return \"Space\";\r\n  const arrowGlyph = ARROW_GLYPHS[segment];\r\n  if (arrowGlyph !== undefined) return arrowGlyph;\r\n  return segment.length === 1 ? segment.toUpperCase() : segment;\r\n}\r\n\r\n/** Renders one binding string (\"mod+shift+ArrowUp\") as an ordered list of display tokens, one per `<Kbd>` chip. */\r\nexport function bindingTokens(binding: string): string[] {\r\n  return binding.split(\"+\").map(displaySegment);\r\n}\r\n\r\n/** One binding string (\"mod+c\") rendered as a row of `<Kbd>` chips (\"⌘\" \"C\"). */\r\nexport function BindingChips({ binding }: { binding: string }): ReactNode {\r\n  return (\r\n    <KbdGroup>\r\n      {bindingTokens(binding).map((token, i) => (\r\n        // tokens within one binding have no other stable identity; index is fine, this list never reorders\r\n        <Kbd key={i}>{token}</Kbd>\r\n      ))}\r\n    </KbdGroup>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-keybindings/binding-label.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid-keybindings/action-groups.ts",
      "content": "import type { DataGridLabels, GridAction } from \"@/registry/default/components/data-grid/data-grid\";\r\n\r\n/** Section order for the dialog; 'other' catches consumer-added actions not in this map. Display strings live in `labels.keybindings.categories` (i18n). */\r\nexport const KEYBINDING_CATEGORIES = [\"navigation\", \"selection\", \"editing\", \"clipboardFill\", \"history\", \"other\"] as const;\r\n\r\nexport type KeybindingCategory = (typeof KEYBINDING_CATEGORIES)[number];\r\n\r\n/** Display label for `category`, from {@link DataGridLabels.keybindings.categories}. */\r\nexport function categoryLabel(category: KeybindingCategory, labels: DataGridLabels): string {\r\n  return labels.keybindings.categories[category];\r\n}\r\n\r\n/** Groups every built-in `GridAction` for the dialog; actions absent here fall back to 'other' so consumer-added actions still render. */\r\nexport const ACTION_CATEGORY: Partial<Record<GridAction, KeybindingCategory>> = {\r\n  moveUp: \"navigation\",\r\n  moveDown: \"navigation\",\r\n  moveLeft: \"navigation\",\r\n  moveRight: \"navigation\",\r\n  retainMoveUp: \"navigation\",\r\n  retainMoveDown: \"navigation\",\r\n  retainMoveLeft: \"navigation\",\r\n  retainMoveRight: \"navigation\",\r\n  scrollActiveIntoView: \"navigation\",\r\n  moveRowStart: \"navigation\",\r\n  moveRowEnd: \"navigation\",\r\n  jumpUp: \"navigation\",\r\n  jumpDown: \"navigation\",\r\n  jumpLeft: \"navigation\",\r\n  jumpRight: \"navigation\",\r\n  moveFirstCell: \"navigation\",\r\n  moveLastCell: \"navigation\",\r\n  pageUp: \"navigation\",\r\n  pageDown: \"navigation\",\r\n\r\n  extendUp: \"selection\",\r\n  extendDown: \"selection\",\r\n  extendLeft: \"selection\",\r\n  extendRight: \"selection\",\r\n  extendJumpUp: \"selection\",\r\n  extendJumpDown: \"selection\",\r\n  extendJumpLeft: \"selection\",\r\n  extendJumpRight: \"selection\",\r\n  extendFirstCell: \"selection\",\r\n  extendLastCell: \"selection\",\r\n  selectRow: \"selection\",\r\n  selectColumn: \"selection\",\r\n  selectAll: \"selection\",\r\n\r\n  edit: \"editing\",\r\n  editReplace: \"editing\",\r\n  commitDown: \"editing\",\r\n  commitUp: \"editing\",\r\n  commitRight: \"editing\",\r\n  commitLeft: \"editing\",\r\n  cancel: \"editing\",\r\n  deleteContents: \"editing\",\r\n  insertRowBelow: \"editing\",\r\n  insertRowAbove: \"editing\",\r\n  duplicateRow: \"editing\",\r\n  deleteRows: \"editing\",\r\n\r\n  fillDown: \"clipboardFill\",\r\n  fillRight: \"clipboardFill\",\r\n\r\n  undo: \"history\",\r\n  redo: \"history\",\r\n};\r\n\r\n/** Category for `action`, defaulting to 'other' for anything not in {@link ACTION_CATEGORY} (consumer-added actions). */\r\nexport function categoryForAction(action: GridAction): KeybindingCategory {\r\n  return ACTION_CATEGORY[action] ?? \"other\";\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-keybindings/action-groups.ts"
    },
    {
      "path": "registry/default/blocks/data-grid-keybindings/action-labels.ts",
      "content": "import type { DataGridLabels, GridAction } from \"@/registry/default/components/data-grid/data-grid\";\r\n\r\n/** Splits \"camelCaseAction\" into \"Camel case action\" for actions missing from {@link DataGridLabels.keybindings.actions}. */\r\nfunction humanizeActionName(action: string): string {\r\n  const spaced = action.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\").toLowerCase();\r\n  return spaced.charAt(0).toUpperCase() + spaced.slice(1);\r\n}\r\n\r\n/** Human label for `action` from {@link DataGridLabels.keybindings.actions}, falling back to a humanized form of its name for consumer-added actions. */\r\nexport function labelForAction(action: GridAction, labels: DataGridLabels): string {\r\n  return labels.keybindings.actions[action] ?? humanizeActionName(action);\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-keybindings/action-labels.ts"
    },
    {
      "path": "components/ui/kbd.tsx",
      "content": "import { cn } from \"@/lib/utils\"\r\n\r\n/** Keycap treatment, owned by the component (not the prose typography plugin) so docs, the keybindings dialog, and consumer apps render identically. */\r\nconst KEYCAP =\r\n  \"rounded-sm border bg-muted shadow-[inset_0_-2px_0_0_color-mix(in_oklab,_currentColor_15%,_transparent)]\"\r\n\r\nfunction Kbd({ className, ...props }: React.ComponentProps<\"kbd\">) {\r\n  return (\r\n    <kbd\r\n      data-slot=\"kbd\"\r\n      className={cn(\r\n        // `not-prose`: the typography plugin's kbd rule (unlayered) would otherwise restyle component-owned keys\r\n        \"pointer-events-none not-prose inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 px-1 font-sans text-xs font-medium text-muted-foreground\",\r\n        KEYCAP,\r\n        \"in-data-[slot=kbd-group]:min-w-0 in-data-[slot=kbd-group]:border-0 in-data-[slot=kbd-group]:bg-transparent in-data-[slot=kbd-group]:px-0.5 in-data-[slot=kbd-group]:shadow-none\",\r\n        \"in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10\",\r\n        \"[&_svg:not([class*='size-'])]:size-3\",\r\n        className\r\n      )}\r\n      {...props}\r\n    />\r\n  )\r\n}\r\n\r\n/** One keycap for a whole binding; inner `Kbd` chips flatten via `in-data-[slot=kbd-group]` and stay separated by the gap. */\r\nfunction KbdGroup({ className, ...props }: React.ComponentProps<\"span\">) {\r\n  return (\r\n    <span\r\n      data-slot=\"kbd-group\"\r\n      className={cn(\r\n        \"pointer-events-none inline-flex h-5 select-none items-center gap-1 px-1 font-sans text-xs font-medium text-muted-foreground\",\r\n        KEYCAP,\r\n        className\r\n      )}\r\n      {...props}\r\n    />\r\n  )\r\n}\r\n\r\nexport { Kbd, KbdGroup }\r\n",
      "type": "registry:component",
      "target": "components/ui/kbd.tsx"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:block"
}
