{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "desktop-8",
  "title": "Desktop 8: keyboard shortcuts editor",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A shortcuts editor: commands grouped and searchable by name or by pressing the keys, a recorder on each row, a warning with Replace when keys are taken, reset per command, Restore defaults, and each platform's notation.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/app-window.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/feel-presets.json",
    "https://ui.ballmac.com/r/shortcut.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/desktop-8/desktop-8.tsx",
      "content": "// Ballmac UI: Desktop 8. https://ui.ballmac.com/blocks/desktop-8\n\"use client\"\n\nimport * as React from \"react\"\nimport { Keyboard, RotateCcw, Search, TriangleAlert } from \"lucide-react\"\n\nimport { AppWindow, AppWindowContent, AppWindowTitleBar } from \"@/components/ballmac/app-window\"\nimport { Button } from \"@/components/ballmac/button\"\nimport { useFeel, type FeelId } from \"@/lib/ballmac/feel\"\nimport { formatShortcut, normalizeShortcut, shortcutFromEvent, usesMacKeys } from \"@/lib/ballmac/shortcut\"\nimport { cn } from \"@/lib/utils\"\n\ntype Desktop8Command = {\n  id: string\n  label: string\n  /** The section it is listed under (\"File\", \"Edit\", \"View\"). */\n  group: string\n  /** The default shortcut, such as \"Mod+Shift+N\". Empty for none. */\n  shortcut: string\n}\n\ntype Desktop8Props = Omit<React.ComponentProps<\"div\">, \"children\" | \"onChange\"> & {\n  /** Which platform the window handles like. Defaults to the page's feel. */\n  feel?: FeelId\n  /** Draw the window frame. Turn off inside a real Tauri or Electron window, or a settings pane. */\n  frame?: boolean\n  /** Every command that can have a shortcut, with its default. */\n  commands?: Desktop8Command[]\n  /** Shortcuts people changed, by command id (\"\" for removed). Pass with onValuesChange to control. */\n  values?: Record<string, string>\n  defaultValues?: Record<string, string>\n  /** Called with every change, holding only the shortcuts that differ from the defaults. */\n  onValuesChange?: (values: Record<string, string>) => void\n  onClose?: () => void\n}\n\nconst sample: Desktop8Command[] = [\n  { id: \"new\", label: \"New note\", group: \"File\", shortcut: \"Mod+N\" },\n  { id: \"new-window\", label: \"New window\", group: \"File\", shortcut: \"Mod+Shift+N\" },\n  { id: \"save\", label: \"Save\", group: \"File\", shortcut: \"Mod+S\" },\n  { id: \"export\", label: \"Export as PDF\", group: \"File\", shortcut: \"\" },\n  { id: \"find\", label: \"Find in note\", group: \"Edit\", shortcut: \"Mod+F\" },\n  { id: \"find-all\", label: \"Find in all notes\", group: \"Edit\", shortcut: \"Mod+Shift+F\" },\n  { id: \"duplicate\", label: \"Duplicate line\", group: \"Edit\", shortcut: \"Mod+Shift+D\" },\n  { id: \"palette\", label: \"Command palette\", group: \"View\", shortcut: \"Mod+K\" },\n  { id: \"sidebar\", label: \"Toggle sidebar\", group: \"View\", shortcut: \"Mod+Alt+S\" },\n  { id: \"focus\", label: \"Focus mode\", group: \"View\", shortcut: \"Mod+Shift+Enter\" },\n]\n\n/**\n * A keyboard shortcuts editor: every command grouped and searchable by name or by pressing the keys, a recorder on each row, a\n * warning with Replace when a combination is already taken, reset per command, Restore defaults, and each platform's notation.\n */\nfunction Desktop8({\n  feel: feelProp,\n  frame = true,\n  commands = sample,\n  values: valuesProp,\n  defaultValues = {},\n  onValuesChange,\n  onClose,\n  className,\n  ...props\n}: Desktop8Props) {\n  const page = useFeel().feel\n  const feel = feelProp ?? page\n  const mac = usesMacKeys(feel)\n  const id = React.useId()\n  const [local, setLocal] = React.useState(defaultValues)\n  const overrides = valuesProp ?? local\n  const [query, setQuery] = React.useState(\"\")\n  const [byKeys, setByKeys] = React.useState<string | null>(null)\n  const [listening, setListening] = React.useState(false)\n  const [recording, setRecording] = React.useState<string | null>(null)\n  const [conflict, setConflict] = React.useState<{ id: string; shortcut: string; other: Desktop8Command } | null>(null)\n  const [announce, setAnnounce] = React.useState(\"\")\n\n  const current = (c: Desktop8Command) => (c.id in overrides ? overrides[c.id]! : c.shortcut)\n  const commit = (next: Record<string, string>) => {\n    // keep only real changes, so the stored settings stay small and defaults can change later\n    const clean = Object.fromEntries(Object.entries(next).filter(([k, v]) => commands.find((c) => c.id === k)?.shortcut !== v))\n    if (!valuesProp) setLocal(clean)\n    onValuesChange?.(clean)\n  }\n  const assign = (cmdId: string, shortcut: string) => commit({ ...overrides, [cmdId]: shortcut })\n\n  function record(cmd: Desktop8Command, shortcut: string) {\n    setRecording(null)\n    const other = commands.find((c) => c.id !== cmd.id && current(c) && normalizeShortcut(current(c), feel) === normalizeShortcut(shortcut, feel))\n    if (other) {\n      setConflict({ id: cmd.id, shortcut, other })\n      return\n    }\n    assign(cmd.id, shortcut)\n    setAnnounce(`${cmd.label}: ${formatShortcut(shortcut, feel)}`)\n  }\n\n  const q = query.trim().toLowerCase()\n  const visible = commands.filter((c) => (byKeys ? normalizeShortcut(current(c), feel) === normalizeShortcut(byKeys, feel) : !q || c.label.toLowerCase().includes(q) || c.group.toLowerCase().includes(q)))\n  const groups = [...new Set(visible.map((c) => c.group))]\n  const changed = Object.keys(overrides).length\n\n  const body = (\n    <div className=\"flex min-h-0 flex-1 flex-col\">\n      <div className=\"flex flex-wrap items-center gap-2 border-b p-3\">\n        <div className=\"bg-background focus-within:ring-ring/50 flex h-8 min-w-0 flex-1 items-center gap-2 rounded-md border px-2.5 focus-within:ring-[3px]\">\n          <Search aria-hidden=\"true\" className=\"text-muted-foreground size-3.5\" />\n          {listening || byKeys ? (\n            <button\n              type=\"button\"\n              autoFocus\n              aria-label=\"Search by keys\"\n              aria-describedby={`${id}-keys-hint`}\n              onKeyDown={(e) => {\n                if (e.key === \"Tab\") return\n                e.preventDefault()\n                if (e.key === \"Escape\") {\n                  setListening(false)\n                  setByKeys(null)\n                  return\n                }\n                const s = shortcutFromEvent(e, mac, false)\n                if (s) {\n                  setByKeys(s)\n                  setListening(false)\n                }\n              }}\n              onClick={() => {\n                setByKeys(null)\n                setListening(true)\n              }}\n              className=\"text-muted-foreground min-w-0 flex-1 text-start text-[13px] outline-none\"\n            >\n              {byKeys ? (\n                <span dir=\"ltr\" className=\"text-foreground font-mono\">{formatShortcut(byKeys, feel)}</span>\n              ) : (\n                \"Press keys…\"\n              )}\n            </button>\n          ) : (\n            <input type=\"search\" aria-label=\"Search commands\" placeholder=\"Search commands\" value={query} onChange={(e) => setQuery(e.target.value)} className=\"placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent text-[13px] outline-none\" />\n          )}\n          <span id={`${id}-keys-hint`} className=\"sr-only\">\n            Press a key combination to find the command that uses it. Escape goes back to searching by name.\n          </span>\n        </div>\n        <Button\n          variant={listening || byKeys ? \"secondary\" : \"outline\"}\n          size=\"sm\"\n          aria-pressed={listening || !!byKeys}\n          onClick={() => {\n            if (listening || byKeys) {\n              setListening(false)\n              setByKeys(null)\n            } else {\n              setQuery(\"\")\n              setListening(true)\n            }\n          }}\n        >\n          <Keyboard aria-hidden=\"true\" />\n          By keys\n        </Button>\n      </div>\n\n      {conflict ? (\n        <div role=\"alert\" className=\"bg-chart-3/10 border-chart-3/30 flex flex-wrap items-center gap-3 border-b px-4 py-2.5 text-[13px]\">\n          <TriangleAlert aria-hidden=\"true\" className=\"text-chart-3 size-4 shrink-0\" />\n          <p className=\"min-w-0 flex-1 text-pretty\">\n            <span dir=\"ltr\" className=\"font-mono font-medium\">{formatShortcut(conflict.shortcut, feel)}</span> is already used by “{conflict.other.label}”.\n          </p>\n          <div className=\"flex gap-2\">\n            <Button variant=\"outline\" size=\"sm\" onClick={() => setConflict(null)}>\n              Cancel\n            </Button>\n            <Button\n              size=\"sm\"\n              onClick={() => {\n                commit({ ...overrides, [conflict.id]: conflict.shortcut, [conflict.other.id]: \"\" })\n                setAnnounce(`Replaced. ${conflict.other.label} has no shortcut now.`)\n                setConflict(null)\n              }}\n            >\n              Replace\n            </Button>\n          </div>\n        </div>\n      ) : null}\n\n      <div className=\"min-h-0 flex-1 overflow-y-auto p-3\">\n        {groups.map((g) => (\n          <section key={g} aria-labelledby={`${id}-${g}`} className=\"mb-4 last:mb-0\">\n            <h3 id={`${id}-${g}`} className=\"text-muted-foreground px-2 pb-1 text-xs font-medium\">\n              {g}\n            </h3>\n            <ul className=\"divide-border bg-card divide-y overflow-hidden rounded-lg border\">\n              {visible\n                .filter((c) => c.group === g)\n                .map((c) => {\n                  const value = current(c)\n                  const isRec = recording === c.id\n                  const custom = c.id in overrides\n                  return (\n                    <li key={c.id} className=\"flex items-center gap-2 px-3 py-1.5\">\n                      <span className=\"min-w-0 flex-1 truncate text-[13px]\">\n                        <span id={`${id}-${c.id}`}>{c.label}</span>\n                        {custom ? <span className=\"text-muted-foreground ms-2 text-xs\">Changed</span> : null}\n                      </span>\n                      <button\n                        type=\"button\"\n                        aria-labelledby={`${id}-${c.id}`}\n                        aria-describedby={`${id}-rec-hint`}\n                        aria-pressed={isRec}\n                        onClick={() => setRecording(isRec ? null : c.id)}\n                        onBlur={() => isRec && setRecording(null)}\n                        onKeyDown={(e) => {\n                          if (!isRec) return\n                          if (e.key === \"Tab\") return setRecording(null)\n                          e.preventDefault()\n                          if (e.key === \"Escape\") return setRecording(null)\n                          if (e.key === \"Backspace\" || e.key === \"Delete\") {\n                            setRecording(null)\n                            assign(c.id, \"\")\n                            setAnnounce(`${c.label}: no shortcut`)\n                            return\n                          }\n                          const s = shortcutFromEvent(e, mac, false)\n                          if (s) record(c, s)\n                        }}\n                        className={cn(\n                          \"border-input bg-background inline-flex h-7 min-w-24 items-center justify-center rounded-md border px-2 font-mono text-xs tabular-nums outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n                          isRec && \"border-ring ring-ring/30 text-muted-foreground ring-[3px]\",\n                          !value && !isRec && \"text-muted-foreground\"\n                        )}\n                      >\n                        <span dir=\"ltr\">{isRec ? \"Type keys…\" : value ? formatShortcut(value, feel) : \"None\"}</span>\n                      </button>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"icon-sm\"\n                        aria-label={`Reset ${c.label}`}\n                        className={cn(\"size-7\", !custom && \"invisible\")}\n                        tabIndex={custom ? undefined : -1}\n                        aria-hidden={custom ? undefined : true}\n                        onClick={() => {\n                          const { [c.id]: _, ...rest } = overrides\n                          commit(rest)\n                          setAnnounce(`${c.label} reset to ${c.shortcut ? formatShortcut(c.shortcut, feel) : \"no shortcut\"}`)\n                        }}\n                      >\n                        <RotateCcw aria-hidden=\"true\" />\n                      </Button>\n                    </li>\n                  )\n                })}\n            </ul>\n          </section>\n        ))}\n        {!visible.length ? (\n          <p className=\"text-muted-foreground py-10 text-center text-sm\">\n            {byKeys ? (\n              <>\n                No command uses <span dir=\"ltr\" className=\"font-mono\">{formatShortcut(byKeys, feel)}</span>.\n              </>\n            ) : (\n              <>No commands match “{query}”.</>\n            )}\n          </p>\n        ) : null}\n        <span id={`${id}-rec-hint`} className=\"sr-only\">\n          Press to record. Then type the new keys; Escape cancels, Delete removes the shortcut.\n        </span>\n      </div>\n\n      <div className=\"flex items-center gap-3 border-t px-4 py-2.5\">\n        <p className=\"text-muted-foreground flex-1 text-xs\">{changed ? (changed === 1 ? \"1 shortcut changed\" : `${changed} shortcuts changed`) : \"All shortcuts are the defaults\"}</p>\n        <Button variant=\"outline\" size=\"sm\" disabled={!changed} onClick={() => { commit({}); setAnnounce(\"Every shortcut restored\") }}>\n          {mac ? \"Restore Defaults\" : \"Restore defaults\"}\n        </Button>\n      </div>\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </p>\n    </div>\n  )\n\n  if (!frame)\n    return (\n      <div data-slot=\"desktop-8\" data-feel={feelProp} className={cn(\"bg-card flex min-h-0 flex-col overflow-hidden rounded-xl border\", className)} {...props}>\n        {body}\n      </div>\n    )\n  return (\n    <AppWindow data-slot=\"desktop-8\" feel={feel} onClose={onClose} className={cn(\"h-[32rem] w-full max-w-xl\", className)} {...props}>\n      <AppWindowTitleBar title={mac ? \"Keyboard Shortcuts\" : \"Keyboard shortcuts\"} icon={<Keyboard />} />\n      <AppWindowContent className=\"flex flex-col overflow-hidden\">{body}</AppWindowContent>\n    </AppWindow>\n  )\n}\n\nexport { Desktop8, type Desktop8Props, type Desktop8Command }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/desktop-8/desktop-8.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "desktop",
      "keyboard",
      "shortcuts",
      "hotkeys",
      "keybindings",
      "preferences",
      "tauri",
      "electron",
      "macos",
      "windows",
      "feel"
    ],
    "version": "1.0.0",
    "updated": "2026-10-17",
    "tests": true,
    "ai": {
      "summary": "A keybindings editor. <Desktop8 commands={[{ id, label, group, shortcut: 'Mod+Shift+N' }]} values={overrides} onValuesChange={(overrides) => save(overrides)} />. values holds only what differs from the defaults ('' means removed). Conflicts are found with normalizeShortcut, so Ctrl and Mod count as one key on Windows.",
      "whenToUse": [
        "The Keyboard Shortcuts pane or window of a desktop or power-user web app",
        "Letting people remap commands that a command palette also runs"
      ],
      "whenNotToUse": [
        "A read-only cheat sheet (use keyboard-shortcuts)",
        "One global shortcut (use the recorder in desktop-1)"
      ],
      "composesWith": [
        "desktop-1",
        "keyboard-shortcuts",
        "command",
        "app-window"
      ],
      "a11y": [
        {
          "keys": "Enter on a shortcut",
          "action": "Starts recording; the next combination is saved, Escape cancels, Delete removes it"
        },
        {
          "keys": "By keys",
          "action": "Press a combination to find the command using it; Escape returns to search by name"
        },
        {
          "keys": "Conflicts",
          "action": "Announced as an alert with Cancel and Replace; every change is announced politely"
        }
      ],
      "customization": [
        "feel: 'macos' | 'windows' (defaults to the page's feel); frame={false} to place it in a settings pane",
        "Pass the effective shortcuts to your hotkey hook or Tauri's global-shortcut plugin (Mod → CommandOrControl)"
      ]
    },
    "statesNone": "Shortcuts are local settings that apply as they change: nothing to load, submit or fail.",
    "examples": [
      "desktop-8-demo",
      "desktop-8-windows"
    ],
    "url": "https://ui.ballmac.com/components/desktop-8"
  },
  "categories": [
    "blocks",
    "desktop"
  ],
  "type": "registry:block"
}