{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shortcuts-sheet",
  "type": "registry:ui",
  "title": "Shortcuts Sheet",
  "description": "Press ? for every shortcut; press any shortcut and its row lights up.",
  "categories": [
    "command"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json",
    "https://www.stealth.pm/r/command-palette.json",
    "https://www.stealth.pm/r/kbd.json"
  ],
  "files": [
    {
      "path": "components/ui/shortcuts-sheet.tsx",
      "content": "\"use client\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport { Toggle } from \"@base-ui/react/toggle\";\nimport { ToggleGroup } from \"@base-ui/react/toggle-group\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Search, X } from \"@/lib/icons\";\nimport { spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\nimport {\n  Kbd,\n  formatShortcut,\n  isTypingTarget,\n  matchShortcut,\n  parseShortcut,\n  resolveStep,\n  useHeldKeys,\n  usePlatform,\n  type Platform,\n} from \"@/components/ui/kbd\";\nimport { CommandHighlight, scoreCommand, usePaletteHotkey } from \"@/components/ui/command-palette\";\n\nexport type Shortcut = {\n  /** \"mod+k\", \"g i\" (a sequence), \"shift+?\"… mod is ⌘ on a Mac and Ctrl elsewhere. */\n  keys: string;\n  label: string;\n  /** More words that should find it. */\n  keywords?: string[];\n};\n\nexport type ShortcutGroup = { heading: string; shortcuts: Shortcut[] };\n\nexport type ShortcutsSheetProps = {\n  groups: ShortcutGroup[];\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Opens and closes the sheet. Ignored while typing in a field. */\n  hotkey?: string | false;\n  hotkeyTarget?: React.RefObject<HTMLElement | null>;\n  /** Write the keys for this platform. Defaults to the reader's, and they can switch. */\n  platform?: Platform;\n  /** Show the macOS / Windows switch in the header. */\n  platformToggle?: boolean;\n  title?: string;\n  container?: Dialog.Portal.Props[\"container\"];\n  className?: string;\n  children?: React.ReactNode;\n};\n\nexport function ShortcutsSheet({\n  groups,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  hotkey = \"?\",\n  hotkeyTarget,\n  platform: platformProp,\n  platformToggle = true,\n  title = \"Keyboard shortcuts\",\n  container,\n  className,\n  children,\n}: ShortcutsSheetProps) {\n  const reduce = !!useReducedMotion();\n  const detected = usePlatform(platformProp);\n  const [chosen, setChosen] = useState<Platform | null>(null);\n  const platform = chosen ?? detected;\n  const [open, setOpen] = useControllableState({ value: openProp, defaultValue: defaultOpen, onChange: onOpenChange });\n  const [instant, setInstant] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const inputRef = useRef<HTMLInputElement>(null);\n  const popupRef = useRef<HTMLDivElement>(null);\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const titleId = useId();\n  const contained = container != null;\n\n  // While the sheet is open, pressing a listed shortcut lights its row and presses its keys.\n  const held = useHeldKeys(open, { ignoreWhileTyping: true });\n\n  usePaletteHotkey({\n    hotkey,\n    target: hotkeyTarget,\n    open,\n    onToggle: () => {\n      setInstant(true);\n      setOpen(!open);\n    },\n  });\n\n  const filtered = useMemo(() => {\n    const q = query.trim();\n    return groups\n      .map((g) => ({\n        heading: g.heading,\n        shortcuts: g.shortcuts\n          .map((s) => {\n            // Keys are searchable by name in both spellings: \"cmd\", \"command\", \"ctrl\", \"shift\"…\n            const spoken = `${formatShortcut(s.keys, \"mac\", { spoken: true })} ${formatShortcut(s.keys, \"other\", { spoken: true })} cmd ${formatShortcut(s.keys, platform)}`;\n            const m = scoreCommand({ id: s.keys, label: s.label, keywords: [g.heading, ...(s.keywords ?? []), ...spoken.split(/[\\s+,]+/)] }, q);\n            return m ? { ...s, ranges: m.ranges } : null;\n          })\n          .filter((s): s is Shortcut & { ranges: [number, number][] } => s !== null),\n      }))\n      .filter((g) => g.shortcuts.length);\n  }, [groups, query, platform]);\n  const count = filtered.reduce((n, g) => n + g.shortcuts.length, 0);\n\n  // A row lights when exactly its keys are held: ⌘↵ lights Send comment, not Open issue (↵) as well.\n  const isActive = (keys: string) =>\n    held.size > 0 &&\n    parseShortcut(keys).some((step) => {\n      const want = resolveStep(step, platform);\n      return want.length === held.size && want.every((k) => held.has(k));\n    });\n  const activeKey = filtered.flatMap((g) => g.shortcuts).find((s) => isActive(s.keys))?.keys;\n\n  useEffect(() => {\n    if (!activeKey) return;\n    scrollRef.current?.querySelector<HTMLElement>(`[data-keys=\"${CSS.escape(activeKey)}\"]`)?.scrollIntoView({ block: \"nearest\", behavior: reduce ? \"auto\" : \"smooth\" });\n  }, [activeKey, reduce]);\n\n  // Listed shortcuts are shown, not run, while the sheet is up: they stop here instead of reaching the app.\n  const listed = useMemo(() => groups.flatMap((g) => g.shortcuts.map((s) => s.keys)), [groups]);\n  const onPopupKeyDown = (e: React.KeyboardEvent) => {\n    const native = e.nativeEvent;\n    if (native.key === \"Escape\" || native.key === \"Tab\") return;\n    // \"/\" jumps to the search field, as it does in most apps.\n    if (native.key === \"/\" && !isTypingTarget(native.target) && !native.metaKey && !native.ctrlKey) {\n      native.preventDefault();\n      e.stopPropagation();\n      inputRef.current?.focus();\n      return;\n    }\n    // The key that opened the sheet closes it again.\n    if (hotkey && matchShortcut(native, hotkey, platform)) return;\n    if (isTypingTarget(native.target) && !native.metaKey && !native.ctrlKey && !native.altKey) return;\n    const hit = listed.some((keys) => parseShortcut(keys).some((step) => resolveStep(step, platform).includes(eventName(native))));\n    if (hit && (native.metaKey || native.ctrlKey || native.altKey || !isTypingTarget(native.target))) {\n      if (native.metaKey || native.ctrlKey || native.altKey) native.preventDefault();\n      e.stopPropagation();\n    }\n  };\n\n  return (\n    <Dialog.Root\n      open={open}\n      onOpenChange={(next, details) => {\n        const ev = details.event as Event | undefined;\n        setInstant(ev instanceof KeyboardEvent || (ev instanceof MouseEvent && ev.detail === 0));\n        setOpen(next);\n      }}\n      onOpenChangeComplete={(isOpen) => !isOpen && setQuery(\"\")}\n    >\n      {children}\n      <Dialog.Portal container={container}>\n        <Dialog.Backdrop\n          data-instant={instant ? \"\" : undefined}\n          className={cn(\n            contained ? \"absolute\" : \"fixed\",\n            \"inset-0 z-(--z-overlay) bg-overlay transition-opacity duration-200 ease-out-quart data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-150 data-instant:transition-none\",\n          )}\n        />\n        <Dialog.Viewport className={cn(contained ? \"absolute\" : \"fixed\", \"inset-0 z-(--z-dialog) flex items-center justify-center p-3 sm:p-4\")}>\n          <Dialog.Popup\n            ref={popupRef}\n            tabIndex={-1}\n            // Focus starts on the sheet, not the field, so pressing any shortcut shows it straight away.\n            initialFocus={popupRef}\n            aria-labelledby={titleId}\n            data-instant={instant ? \"\" : undefined}\n            onKeyDown={onPopupKeyDown}\n            className={cn(\n              \"@container relative flex max-h-full w-full max-w-[720px] min-w-0 flex-col overflow-hidden rounded-2xl border border-line-2 bg-raised text-fg shadow-pop outline-none\",\n              \"origin-center transition-[opacity,scale,translate] duration-[220ms] ease-out-expo\",\n              \"data-starting-style:translate-y-1 data-starting-style:scale-[0.98] data-starting-style:opacity-0\",\n              \"data-ending-style:scale-[0.99] data-ending-style:opacity-0 data-ending-style:duration-[140ms] data-ending-style:ease-out-quart\",\n              \"motion-reduce:data-starting-style:translate-y-0 motion-reduce:data-starting-style:scale-100 motion-reduce:data-ending-style:scale-100\",\n              \"data-instant:transition-none\",\n              contained ? \"h-full max-h-[560px]\" : \"h-[min(600px,85dvh)]\",\n              className,\n            )}\n          >\n            <div className=\"flex shrink-0 flex-wrap items-center gap-x-2 gap-y-2.5 border-b border-line px-4 py-3\">\n              <Dialog.Title id={titleId} className=\"order-1 mr-auto min-w-0 flex-1 text-[14px] @max-[479px]:basis-[calc(100%-2.5rem)] font-medium tracking-[-0.015em] text-balance text-fg\">\n                {title}\n              </Dialog.Title>\n              {platformToggle && <PlatformSwitch value={platform} onChange={setChosen} reduce={reduce} className=\"order-5 @[480px]:order-2\" />}\n              <Dialog.Close\n                aria-label=\"Close\"\n                className={cn(\n                  \"relative order-3 -mr-1 grid size-7 shrink-0 place-items-center rounded-md text-fg-3 outline-none\",\n                  \"transition-[background-color,color,scale] duration-150 hover:bg-hover hover:text-fg active:scale-[0.92]\",\n                  \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n                  \"after:absolute after:-inset-2 after:content-[''] pointer-fine:after:hidden\",\n                )}\n              >\n                <X size={16} />\n              </Dialog.Close>\n              <label className=\"group/search relative order-4 flex h-8 w-full min-w-0 items-center @max-[479px]:w-auto @max-[479px]:flex-1 rounded-lg border border-line-2 bg-frame transition-[border-color,box-shadow] duration-150 focus-within:border-fg-4 focus-within:ring-3 focus-within:ring-fg/8 hover:border-fg-4\">\n                <Search size={14} className=\"pointer-events-none absolute left-2.5 text-fg-3\" />\n                <input\n                  ref={inputRef}\n                  type=\"search\"\n                  value={query}\n                  onChange={(e) => setQuery(e.target.value)}\n                  onKeyDown={(e) => {\n                    // Escape clears a search before it closes the sheet.\n                    if (e.key === \"Escape\" && query) {\n                      e.preventDefault();\n                      e.stopPropagation();\n                      setQuery(\"\");\n                    }\n                  }}\n                  placeholder=\"Search\"\n                  aria-label=\"Search shortcuts\"\n                  aria-controls={`${titleId}-list`}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                  enterKeyHint=\"search\"\n                  className=\"peer h-full min-w-0 flex-1 bg-transparent pr-2.5 pl-8 text-base text-fg outline-none placeholder:text-fg-4 sm:text-[13px] [&::-webkit-search-cancel-button]:hidden\"\n                />\n                <Kbd keys=\"/\" size=\"sm\" className=\"mr-1.5 transition-opacity duration-150 peer-focus:opacity-0 pointer-coarse:hidden @max-[479px]:hidden\" />\n              </label>\n            </div>\n\n            <div ref={scrollRef} id={`${titleId}-list`} className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-1 pb-4\">\n              {count === 0 ? (\n                <div className=\"flex flex-col items-center gap-3 py-14 text-center\">\n                  <p className=\"max-w-full truncate text-[13px] text-fg-2\">\n                    No shortcuts match <span className=\"text-fg\">“{query.trim()}”</span>\n                  </p>\n                  <button\n                    type=\"button\"\n                    onClick={() => {\n                      setQuery(\"\");\n                      inputRef.current?.focus();\n                    }}\n                    className={cn(\n                      \"inline-flex h-7 items-center rounded-md border border-line-2 bg-raised px-2.5 text-[12px] font-medium text-fg shadow-[var(--shadow)]\",\n                      \"transition-[background-color,border-color,scale] duration-150 hover:border-fg-4 hover:bg-hover active:scale-[0.97]\",\n                      \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n                    )}\n                  >\n                    Clear search\n                  </button>\n                </div>\n              ) : (\n                <div className=\"gap-x-8 @[560px]:columns-2\">\n                  {filtered.map((g) => (\n                    <section key={g.heading} aria-label={g.heading} className=\"break-inside-avoid pt-4\">\n                      <h3 className=\"pb-1.5 font-mono text-2xs uppercase tracking-[0.08em] text-fg-3\">{g.heading}</h3>\n                      <ul>\n                        {g.shortcuts.map((s) => {\n                          const active = activeKey === s.keys || isActive(s.keys);\n                          return (\n                            <li\n                              key={s.keys + s.label}\n                              data-keys={s.keys}\n                              data-active={active ? \"\" : undefined}\n                              className={cn(\n                                \"-mx-2 flex min-h-8 items-center justify-between gap-3 rounded-md px-2 py-1 text-[13px] text-fg-2\",\n                                \"transition-[background-color,color] duration-150 data-active:bg-fg/[0.07] data-active:text-fg data-active:duration-50\",\n                              )}\n                            >\n                              <span className=\"min-w-0\">\n                                <CommandHighlight text={s.label} ranges={s.ranges} />\n                              </span>\n                              <Kbd keys={s.keys} size=\"sm\" platform={platform} pressed={held} className=\"shrink-0\" />\n                            </li>\n                          );\n                        })}\n                      </ul>\n                    </section>\n                  ))}\n                </div>\n              )}\n            </div>\n\n            <div className=\"flex h-10 shrink-0 items-center gap-1.5 border-t border-line px-4 text-[11.5px] text-fg-3 pointer-coarse:hidden\">\n              <span className=\"truncate\">Press a shortcut to find it. It won’t run.</span>\n            </div>\n            <span role=\"status\" className=\"sr-only\">\n              {query.trim() ? (count === 0 ? \"No shortcuts match\" : `${count} ${count === 1 ? \"shortcut\" : \"shortcuts\"}`) : \"\"}\n            </span>\n          </Dialog.Popup>\n        </Dialog.Viewport>\n      </Dialog.Portal>\n    </Dialog.Root>\n  );\n}\n\nfunction eventName(e: KeyboardEvent) {\n  if (/^Key[A-Z]$/.test(e.code)) return e.code.slice(3).toLowerCase();\n  if (/^Digit\\d$/.test(e.code)) return e.code.slice(5);\n  return e.key.length === 1 ? e.key.toLowerCase() : e.key.toLowerCase().replace(/^arrow/, \"\");\n}\n\nfunction PlatformSwitch({ value, onChange, reduce, className }: { value: Platform; onChange: (p: Platform) => void; reduce: boolean; className?: string }) {\n  const id = useId();\n  return (\n    <ToggleGroup\n      aria-label=\"Show shortcuts for\"\n      value={[value]}\n      onValueChange={(v) => v[0] && onChange(v[0] as Platform)}\n      className={cn(\"flex shrink-0 rounded-lg border border-line bg-frame p-0.5\", className)}\n    >\n      {(\n        [\n          [\"mac\", \"Mac\"],\n          [\"other\", \"Windows\"],\n        ] as const\n      ).map(([v, label]) => (\n        <Toggle\n          key={v}\n          value={v}\n          className={cn(\n            \"relative h-6 rounded-md px-2 text-[11.5px] font-medium text-fg-3 outline-none select-none\",\n            \"transition-[color,scale] duration-150 hover:text-fg-2 active:scale-[0.96] data-pressed:text-fg\",\n            \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n          )}\n        >\n          {value === v && (\n            <motion.span\n              layoutId={`${id}-platform`}\n              transition={reduce ? { duration: 0 } : spring.snappy}\n              className=\"absolute inset-0 rounded-md border border-line-2 bg-raised shadow-[var(--shadow)]\"\n            />\n          )}\n          <span className=\"relative\">{label}</span>\n        </Toggle>\n      ))}\n    </ToggleGroup>\n  );\n}\n\nexport type ShortcutsSheetTriggerProps = Omit<Dialog.Trigger.Props, \"children\"> & { label?: string; shortcut?: string };\n\n/** A quiet button that opens the sheet and shows its key. */\nexport function ShortcutsSheetTrigger({ label = \"Keyboard shortcuts\", shortcut = \"?\", className, ...rest }: ShortcutsSheetTriggerProps) {\n  return (\n    <Dialog.Trigger\n      className={cn(\n        \"relative inline-flex h-8 select-none items-center gap-2 rounded-lg px-2.5 text-[12.5px] text-fg-2 outline-none\",\n        \"transition-[background-color,color,scale] duration-150 hover:bg-hover hover:text-fg active:scale-[0.97]\",\n        \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n        typeof className === \"string\" ? className : undefined,\n      )}\n      {...rest}\n    >\n      {label}\n      <Kbd keys={shortcut} size=\"sm\" />\n    </Dialog.Trigger>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/shortcuts-sheet"
}
