{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shortcut-sheet",
  "type": "registry:component",
  "title": "Shortcut Sheet",
  "description": "A keyboard cheat sheet that slides up when you press ? inside your app: searchable by action or key, grouped, with sequences and Mac or Windows keys.",
  "categories": [
    "component",
    "Workspace"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/kbd.json"
  ],
  "files": [
    {
      "path": "registry/devl/shortcut-sheet.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { SearchIcon, XIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type KeyboardEvent, type ReactNode, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Kbd } from \"@/components/ui/kbd\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface Shortcut {\n  label: string;\n  keys: string[][];\n  keywords?: string[];\n}\n\nexport interface ShortcutGroup {\n  title: string;\n  shortcuts: Shortcut[];\n}\n\nexport type Platform = \"mac\" | \"windows\";\n\nexport interface ShortcutSheetProps {\n  groups: ShortcutGroup[];\n  children: ReactNode;\n  defaultPlatform?: Platform;\n  title?: string;\n  className?: string;\n}\n\nconst MAC: Record<string, string> = { mod: \"⌘\", shift: \"⇧\", alt: \"⌥\", ctrl: \"⌃\", enter: \"↵\", esc: \"esc\", backspace: \"⌫\", up: \"↑\", down: \"↓\", left: \"←\", right: \"→\" };\nconst WIN: Record<string, string> = { mod: \"Ctrl\", shift: \"Shift\", alt: \"Alt\", ctrl: \"Ctrl\", enter: \"Enter\", esc: \"Esc\", backspace: \"Backspace\", up: \"↑\", down: \"↓\", left: \"←\", right: \"→\" };\nconst SPOKEN: Record<string, string> = { mod: \"Command\", shift: \"Shift\", alt: \"Option\", ctrl: \"Control\", enter: \"Return\", esc: \"Escape\", backspace: \"Delete\", up: \"Up\", down: \"Down\", left: \"Left\", right: \"Right\" };\n\nconst keyLabel = (token: string, platform: Platform) => (platform === \"mac\" ? MAC : WIN)[token.toLowerCase()] ?? token.toUpperCase();\n\nfunction isTyping(el: EventTarget | null) {\n  if (!(el instanceof HTMLElement)) return false;\n  return el.isContentEditable || el.tagName === \"INPUT\" || el.tagName === \"TEXTAREA\" || el.tagName === \"SELECT\";\n}\n\nfunction Highlight({ text, query }: { text: string; query: string }) {\n  const i = query ? text.toLowerCase().indexOf(query) : -1;\n  if (i < 0) return <>{text}</>;\n  return (\n    <>\n      {text.slice(0, i)}\n      <mark className=\"rounded-[calc(var(--radius)*0.3)] bg-amber-400/30 text-foreground\">{text.slice(i, i + query.length)}</mark>\n      {text.slice(i + query.length)}\n    </>\n  );\n}\n\nexport function ShortcutSheet({ groups, children, defaultPlatform, title = \"Keyboard shortcuts\", className }: ShortcutSheetProps) {\n  const reduce = useReducedMotion();\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const [platform, setPlatform] = useState<Platform>(defaultPlatform ?? \"mac\");\n  const rootRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const titleId = useId();\n\n  useEffect(() => {\n    if (defaultPlatform) return;\n    const nav = navigator as Navigator & { userAgentData?: { platform?: string } };\n    const name = nav.userAgentData?.platform ?? nav.platform ?? \"\";\n    if (!/mac|iphone|ipad/i.test(name)) setPlatform(\"windows\");\n  }, [defaultPlatform]);\n\n  useEffect(() => {\n    if (open) inputRef.current?.focus();\n  }, [open]);\n\n  const close = () => {\n    setOpen(false);\n    setQuery(\"\");\n    rootRef.current?.focus();\n  };\n\n  const q = query.trim().toLowerCase();\n  const filtered = useMemo(() => {\n    if (!q) return groups;\n    const words = q.split(/[\\s+]+/).filter(Boolean);\n    return groups\n      .map((g) => ({\n        ...g,\n        shortcuts: g.shortcuts.filter((s) => {\n          const keyText = s.keys.flat().flatMap((k) => [k.toLowerCase(), keyLabel(k, \"windows\").toLowerCase(), SPOKEN[k.toLowerCase()]?.toLowerCase() ?? \"\"]);\n          const hay = [s.label.toLowerCase(), g.title.toLowerCase(), ...(s.keywords ?? []).map((k) => k.toLowerCase())].join(\" \");\n          return hay.includes(q) || words.every((w) => keyText.includes(w) || hay.includes(w));\n        }),\n      }))\n      .filter((g) => g.shortcuts.length);\n  }, [groups, q]);\n  const total = filtered.reduce((n, g) => n + g.shortcuts.length, 0);\n\n  const onRootKey = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (open) {\n      if (event.key === \"Escape\") {\n        event.preventDefault();\n        event.stopPropagation();\n        close();\n      }\n      return;\n    }\n    if (event.key === \"?\" && !event.metaKey && !event.ctrlKey && !isTyping(event.target)) {\n      event.preventDefault();\n      setOpen(true);\n    }\n  };\n\n  const ease = [0.23, 1, 0.32, 1] as const;\n\n  return (\n    <div\n      aria-label=\"Workspace. Press question mark for keyboard shortcuts.\"\n      className={cn(\n        \"relative isolate overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-xs/5 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        className,\n      )}\n      onKeyDown={onRootKey}\n      onPointerDown={(event) => {\n        if (!isTyping(event.target) && !(event.target as HTMLElement).closest(\"button,a,[tabindex]:not([data-sheet-root])\")) {\n          event.preventDefault();\n          rootRef.current?.focus();\n        }\n      }}\n      data-sheet-root=\"\"\n      ref={rootRef}\n      role=\"region\"\n      tabIndex={0}\n    >\n      <div aria-hidden={open || undefined} inert={open || undefined}>\n        {children}\n        <div className=\"flex items-center justify-between border-t px-4 py-2 text-muted-foreground text-xs\">\n          <span>Click anywhere in here, then press</span>\n          <button\n            className=\"flex cursor-pointer items-center gap-1.5 rounded-md px-1.5 py-1 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n            onClick={() => setOpen(true)}\n            type=\"button\"\n          >\n            <Kbd>?</Kbd>\n            <span>Shortcuts</span>\n          </button>\n        </div>\n      </div>\n\n      <AnimatePresence>\n        {open && (\n          <motion.div\n            animate={{ opacity: 1 }}\n            className=\"absolute inset-0 z-10 flex items-end bg-background/40 p-2 backdrop-blur-[2px]\"\n            exit={{ opacity: 0, transition: { duration: 0.15 } }}\n            initial={{ opacity: 0 }}\n            key=\"scrim\"\n            onPointerDown={(event) => {\n              if (event.target === event.currentTarget) {\n                event.preventDefault();\n                close();\n              }\n            }}\n            transition={{ duration: 0.18 }}\n          >\n            <motion.div\n              animate={{ opacity: 1, y: 0, scale: 1 }}\n              aria-labelledby={titleId}\n              aria-modal=\"true\"\n              className=\"flex h-full w-full flex-col overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-lg/5\"\n              exit={reduce ? { opacity: 0 } : { opacity: 0, y: 12, scale: 0.98, transition: { duration: 0.15, ease: \"easeOut\" } }}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, y: 16, scale: 0.98 }}\n              onKeyDown={(event) => {\n                if (event.key !== \"Tab\") return;\n                const items = [...event.currentTarget.querySelectorAll<HTMLElement>(\"input,button\")];\n                const first = items[0];\n                const last = items[items.length - 1];\n                if (event.shiftKey && document.activeElement === first) {\n                  event.preventDefault();\n                  last?.focus();\n                } else if (!event.shiftKey && document.activeElement === last) {\n                  event.preventDefault();\n                  first?.focus();\n                }\n              }}\n              role=\"dialog\"\n              transition={{ duration: 0.24, ease }}\n            >\n              <div className=\"flex items-center gap-2 border-b px-3 py-2.5\">\n                <h2 className=\"sr-only\" id={titleId}>\n                  {title}\n                </h2>\n                <SearchIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n                <input\n                  aria-label=\"Search shortcuts\"\n                  className=\"h-7 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground\"\n                  onChange={(e) => setQuery(e.target.value)}\n                  placeholder=\"Search shortcuts…\"\n                  ref={inputRef}\n                  value={query}\n                />\n                <PlatformSwitch onChange={setPlatform} value={platform} />\n                <button\n                  aria-label=\"Close shortcuts\"\n                  className=\"-me-1 flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n                  onClick={close}\n                  type=\"button\"\n                >\n                  <XIcon className=\"size-4\" />\n                </button>\n              </div>\n              <div className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 pt-1 pb-4 [mask-image:linear-gradient(to_bottom,black_calc(100%-20px),transparent)]\">\n                {filtered.length === 0 ? (\n                  <div className=\"flex h-full flex-col items-center justify-center gap-1 px-6 pb-6 text-center\">\n                    <p className=\"font-medium text-sm\">No shortcuts for “{query.trim()}”</p>\n                    <p className=\"text-muted-foreground text-xs\">Try an action like “archive”, or a key like “shift”.</p>\n                  </div>\n                ) : (\n                  filtered.map((g) => (\n                    <section className=\"mt-2\" key={g.title}>\n                      <h3 className=\"px-1 pb-1 font-medium text-[11px] text-muted-foreground uppercase tracking-wide\">{g.title}</h3>\n                      <ul>\n                        {g.shortcuts.map((s) => (\n                          <li className=\"flex min-h-8 items-center justify-between gap-3 rounded-md px-1 py-1 text-sm\" key={s.label}>\n                            <span className=\"min-w-0 truncate\">\n                              <Highlight query={q} text={s.label} />\n                            </span>\n                            <Keys keys={s.keys} platform={platform} reduce={!!reduce} />\n                          </li>\n                        ))}\n                      </ul>\n                    </section>\n                  ))\n                )}\n              </div>\n              <p aria-live=\"polite\" className=\"sr-only\">\n                {q ? `${total} shortcut${total === 1 ? \"\" : \"s\"} found` : \"\"}\n              </p>\n            </motion.div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction Keys({ keys, platform, reduce }: { keys: string[][]; platform: Platform; reduce: boolean }) {\n  const spoken = keys.map((combo) => combo.map((k) => SPOKEN[k.toLowerCase()] ?? k).join(\" \")).join(\", then \");\n  return (\n    <span aria-label={spoken} className=\"flex shrink-0 items-center gap-1.5\" role=\"img\">\n      {keys.map((combo, i) => (\n        <span className=\"flex items-center gap-1.5\" key={combo.join(\"+\")}>\n          {i > 0 && <span className=\"text-[11px] text-muted-foreground\">then</span>}\n          <span className=\"flex items-center gap-0.5\">\n            {combo.map((k) => (\n              <motion.span\n                animate={{ opacity: 1 }}\n                className=\"inline-flex\"\n                initial={reduce ? false : { opacity: 0.2 }}\n                key={`${platform}-${k}`}\n                transition={{ duration: 0.18 }}\n              >\n                <Kbd className=\"h-5.5 min-w-5.5 border border-border bg-muted/60 px-1.5 font-sans text-[11px] text-foreground/80 shadow-[0_1px_0_var(--color-border)]\">\n                  {keyLabel(k, platform)}\n                </Kbd>\n              </motion.span>\n            ))}\n          </span>\n        </span>\n      ))}\n    </span>\n  );\n}\n\nfunction PlatformSwitch({ value, onChange }: { value: Platform; onChange: (p: Platform) => void }) {\n  const id = useId();\n  return (\n    <div aria-label=\"Show keys for\" className=\"relative flex shrink-0 rounded-md bg-muted p-0.5\" role=\"radiogroup\">\n      {([\"mac\", \"windows\"] as const).map((p) => (\n        <button\n          aria-checked={value === p}\n          className={cn(\n            \"relative z-0 h-6 cursor-pointer rounded-[calc(var(--radius-md)-2px)] px-2 font-medium text-xs outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n            value === p ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n          )}\n          key={p}\n          onClick={() => onChange(p)}\n          role=\"radio\"\n          type=\"button\"\n        >\n          {value === p && (\n            <motion.span\n              className=\"absolute inset-0 -z-10 rounded-[calc(var(--radius-md)-2px)] bg-background shadow-sm/5 dark:bg-input\"\n              layoutId={`${id}-platform`}\n              transition={{ type: \"spring\", stiffness: 500, damping: 40 }}\n            />\n          )}\n          {p === \"mac\" ? \"Mac\" : \"Windows\"}\n        </button>\n      ))}\n    </div>\n  );\n}\n"
    }
  ]
}