{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"kbd","type":"registry:ui","title":"Kbd","description":"Keys as small raised caps: one shortcut per kbd, ⌘ on a Mac and Ctrl elsewhere, with spoken names for symbols.","dependencies":["clsx","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/kbd.tsx","type":"registry:ui","target":"components/ui/kbd.tsx","content":"\"use client\";\n\nimport { useSyncExternalStore, type ComponentProps } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./kbd.css\";\n\nexport type KbdPlatform = \"mac\" | \"other\";\n\n/* Each key: the glyph people see on that platform, and the word a screen reader says. */\nconst named: Record<string, { mac: string; other: string; spoken: string; aria: string }> = {\n  mod: { mac: \"⌘\", other: \"Ctrl\", spoken: \"Command\", aria: \"Meta\" },\n  cmd: { mac: \"⌘\", other: \"⌘\", spoken: \"Command\", aria: \"Meta\" },\n  meta: { mac: \"⌘\", other: \"Win\", spoken: \"Command\", aria: \"Meta\" },\n  ctrl: { mac: \"⌃\", other: \"Ctrl\", spoken: \"Control\", aria: \"Control\" },\n  alt: { mac: \"⌥\", other: \"Alt\", spoken: \"Option\", aria: \"Alt\" },\n  option: { mac: \"⌥\", other: \"Alt\", spoken: \"Option\", aria: \"Alt\" },\n  shift: { mac: \"⇧\", other: \"Shift\", spoken: \"Shift\", aria: \"Shift\" },\n  enter: { mac: \"↵\", other: \"Enter\", spoken: \"Enter\", aria: \"Enter\" },\n  return: { mac: \"↵\", other: \"Enter\", spoken: \"Return\", aria: \"Enter\" },\n  esc: { mac: \"Esc\", other: \"Esc\", spoken: \"Escape\", aria: \"Escape\" },\n  escape: { mac: \"Esc\", other: \"Esc\", spoken: \"Escape\", aria: \"Escape\" },\n  tab: { mac: \"⇥\", other: \"Tab\", spoken: \"Tab\", aria: \"Tab\" },\n  backspace: { mac: \"⌫\", other: \"Backspace\", spoken: \"Backspace\", aria: \"Backspace\" },\n  delete: { mac: \"⌦\", other: \"Del\", spoken: \"Delete\", aria: \"Delete\" },\n  space: { mac: \"Space\", other: \"Space\", spoken: \"Space\", aria: \"Space\" },\n  up: { mac: \"↑\", other: \"↑\", spoken: \"Up arrow\", aria: \"ArrowUp\" },\n  down: { mac: \"↓\", other: \"↓\", spoken: \"Down arrow\", aria: \"ArrowDown\" },\n  left: { mac: \"←\", other: \"←\", spoken: \"Left arrow\", aria: \"ArrowLeft\" },\n  right: { mac: \"→\", other: \"→\", spoken: \"Right arrow\", aria: \"ArrowRight\" },\n  pageup: { mac: \"PgUp\", other: \"PgUp\", spoken: \"Page up\", aria: \"PageUp\" },\n  pagedown: { mac: \"PgDn\", other: \"PgDn\", spoken: \"Page down\", aria: \"PageDown\" },\n  home: { mac: \"Home\", other: \"Home\", spoken: \"Home\", aria: \"Home\" },\n  end: { mac: \"End\", other: \"End\", spoken: \"End\", aria: \"End\" },\n};\n\n/** \"mod+shift+k\" or [\"mod\", \"K\"] → [\"mod\", \"shift\", \"k\"]. A lone \"+\" stays a key. */\nfunction tokens(keys: string | readonly string[]) {\n  if (typeof keys !== \"string\") return [...keys];\n  if (keys === \"+\") return [\"+\"];\n  return keys.split(/\\+(?!$)/u).map((key) => key.trim()).filter(Boolean);\n}\n\nfunction describe(key: string, platform: KbdPlatform) {\n  const known = named[key.toLowerCase()];\n  if (known) return { glyph: known[platform], spoken: known.spoken, aria: known.aria };\n  const glyph = key.length === 1 ? key.toUpperCase() : key;\n  return { glyph, spoken: glyph, aria: glyph };\n}\n\nfunction detect(): KbdPlatform {\n  const nav = navigator as Navigator & { userAgentData?: { platform?: string } };\n  const name = nav.userAgentData?.platform ?? nav.platform ?? \"\";\n  return /mac|iphone|ipad|ipod/iu.test(name) ? \"mac\" : \"other\";\n}\n\n/** The platform's modifier names. The server renders \"other\"; Macs switch to ⌘ after hydration. */\nexport function useKbdPlatform(platform?: KbdPlatform): KbdPlatform {\n  const detected = useSyncExternalStore(\n    () => () => {},\n    detect,\n    () => \"other\" as const,\n  );\n  return platform ?? detected;\n}\n\n/** The value for `aria-keyshortcuts` on the control the keys trigger, e.g. \"Meta+K\". */\nexport function toAriaKeyshortcuts(keys: string | readonly string[], platform: KbdPlatform = \"other\") {\n  return tokens(keys)\n    .map((key) => {\n      if (key.toLowerCase() === \"mod\") return platform === \"mac\" ? \"Meta\" : \"Control\";\n      return describe(key, platform).aria;\n    })\n    .join(\"+\");\n}\n\nexport type KbdProps = Omit<ComponentProps<\"kbd\">, \"children\"> & {\n  /** One key or a combination: \"mod+k\", \"shift+enter\", [\"alt\", \"↑\"]. `mod` is ⌘ on a Mac and Ctrl elsewhere. */\n  keys?: string | readonly string[];\n  /** Free text instead of keys, e.g. \"G then D\". */\n  children?: string;\n  /** `inline` sits quietly inside a button, menu row or tooltip and takes its colour. */\n  variant?: \"default\" | \"inline\";\n  size?: \"sm\" | \"md\";\n  /** Force a platform instead of detecting it, e.g. in documentation. */\n  platform?: KbdPlatform;\n};\n\n/**\n * Keys drawn as small raised caps. A combination is one `kbd` holding a cap\n * per key, so it reads as a single shortcut; symbols such as ⌘ carry their\n * spoken name for screen readers.\n */\nexport function Kbd({ keys, children, variant = \"default\", size = \"md\", platform, className, ...props }: KbdProps) {\n  const resolved = useKbdPlatform(platform);\n  const list = keys !== undefined ? tokens(keys) : children !== undefined ? [children] : [];\n  const parts = list.map((key) => describe(key, resolved));\n  const shared = { \"data-variant\": variant, \"data-size\": size } as const;\n  if (parts.length === 1) {\n    return (\n      <kbd data-slot=\"kbd\" {...shared} className={className} {...props}>\n        <Glyph {...parts[0]!} />\n      </kbd>\n    );\n  }\n  return (\n    <kbd data-slot=\"kbd-group\" {...shared} className={cn(className)} {...props}>\n      {parts.map((part, index) => (\n        <kbd key={index} data-slot=\"kbd\" {...shared}>\n          {index > 0 && <span className=\"sr-only\"> </span>}\n          <Glyph {...part} />\n        </kbd>\n      ))}\n    </kbd>\n  );\n}\n\nfunction Glyph({ glyph, spoken }: { glyph: string; spoken: string }) {\n  if (glyph === spoken) return glyph;\n  return (\n    <>\n      <span aria-hidden=\"true\">{glyph}</span>\n      <span className=\"sr-only\">{spoken}</span>\n    </>\n  );\n}\n"},{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/kbd.css","type":"registry:file","target":"components/ui/kbd.css","content":"/* kbd.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Key caps: a small raised object with a lip on the bottom edge. */\n  [data-slot=\"kbd-group\"] { display: inline-flex; align-items: center; gap: 3px; font: inherit; }\n  [data-slot=\"kbd\"] {\n    display: inline-flex; align-items: center; justify-content: center;\n    min-width: 20px; height: 20px; padding: 0 5px;\n    border-radius: var(--radius-mark);\n    background: var(--raised);\n    box-shadow: inset 0 0 0 1px var(--border), inset 0 -1px 0 var(--border-strong);\n    color: var(--muted-foreground);\n    font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px; line-height: 1; font-weight: 500;\n    font-variant-numeric: tabular-nums; white-space: nowrap;\n  }\n  /* A key hint inside a search box or field is a soft fill: the box already has the border. */\n  :is([data-slot=\"input-group\"], [data-slot=\"search\"], [data-slot$=\"-search\"], [data-slot=\"command-input\"]) [data-slot=\"kbd\"] { background: var(--muted); box-shadow: none; border: 0; }\n  [data-slot=\"kbd\"][data-size=\"sm\"] { min-width: 18px; height: 18px; padding: 0 4px; font-size: 12px; }\n  [data-slot=\"kbd\"][data-variant=\"inline\"] {\n    background: transparent; color: inherit; opacity: 0.72;\n    box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 26%, transparent);\n  }\n  [data-slot=\"kbd-group\"][data-variant=\"inline\"] { opacity: 1; }\n}\n"}],"categories":["actions"],"meta":{"tier":"free","license":"MIT","family":"actions","docs":"/docs/components/kbd"}}