{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"kbd-clean","type":"registry:ui","title":"Kbd (Clean)","description":"A key chip for shortcuts – one key or a combo in one chip, ⌘ on a Mac and Ctrl elsewhere from a single mod key, spelled out for screen readers","dependencies":[],"files":[{"path":"registry/kbd-clean/kbd-clean.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useSyncExternalStore, type ComponentPropsWithRef } from \"react\";\n\n/*\n * Kbd – a single self-contained file, clean style.\n * A key chip for shortcuts in menus, tooltips and help lists: one key (Esc), or a combo in one chip (⌘K). The key\n * `mod` is ⌘ on a Mac and Ctrl everywhere else; screen readers hear the keys spelled out (\"Command K\").\n * The styles are injected below, so no extra files are needed.\n */\n\nexport type KbdPlatform = \"mac\" | \"other\";\n\nexport type KbdProps = ComponentPropsWithRef<\"kbd\"> & {\n  /**\n   * One key or a combo, e.g. \"esc\" or [\"mod\", \"K\"]. Named keys: mod, cmd, ctrl, alt (option), shift, enter, esc, tab,\n   * space, backspace, delete, up, down, left, right, pageup, pagedown, home, end – anything else is shown as it is.\n   * Leave it out to show the children as one plain key.\n   */\n  keys?: string | readonly string[];\n  /** sm: 16px high, 10.5px text – in menus and hints · md: 20px high, 12px text – in lists and help */\n  size?: \"sm\" | \"md\";\n  /** Draw the keys for a Mac (⌘ ⌥ ⇧) or for Windows and Linux (Ctrl, Alt, Shift) – found out on its own when left out */\n  platform?: KbdPlatform;\n};\n\nexport function Kbd({ keys, size = \"sm\", platform, className, children, ...rest }: KbdProps) {\n  const detected = usePlatform();\n  const os = platform === \"mac\" || platform === \"other\" ? platform : detected;\n  const list = keys == null ? [] : (typeof keys === \"string\" ? [keys] : [...keys]).filter((k) => typeof k === \"string\" && k.trim() !== \"\");\n  const parts = list.map((k) => describe(k, os));\n  // Nothing to show – no keys, no children: no empty chip either\n  if (!parts.length && (children == null || children === false || children === \"\")) return null;\n\n  return (\n    <>\n      {/* React 19 hoists this into <head> once, no matter how many chips render */}\n      <style href=\"ui-kbd-clean\" precedence=\"default\">\n        {css}\n      </style>\n      <kbd\n        className={[\"ui-kbdc\", className].filter(Boolean).join(\" \")}\n        data-size={size === \"md\" ? \"md\" : \"sm\"}\n        data-os={os}\n        {...rest}\n      >\n        {parts.length ? (\n          <>\n            {/* The glyphs are for the eye; the spelled-out names for screen readers (\"Command Shift P\") */}\n            <span className=\"ui-kbdc__keys\" aria-hidden=\"true\">\n              {parts.map((p, i) => (\n                <span key={i} className=\"ui-kbdc__key\" data-glyph={p.glyph ? \"\" : undefined}>\n                  {p.label}\n                </span>\n              ))}\n            </span>\n            <span className=\"ui-kbdc__sr\">{parts.map((p) => p.spoken).join(\" \")}</span>\n          </>\n        ) : (\n          children\n        )}\n      </kbd>\n    </>\n  );\n}\n\n/* ---------- Keys ---------- */\n\ntype Named = { mac: string; other: string; spoken: [mac: string, other: string]; glyph?: boolean };\n\nconst NAMED: Record<string, Named> = {\n  mod: { mac: \"⌘\", other: \"Ctrl\", spoken: [\"Command\", \"Control\"], glyph: true },\n  cmd: { mac: \"⌘\", other: \"⌘\", spoken: [\"Command\", \"Command\"], glyph: true },\n  ctrl: { mac: \"⌃\", other: \"Ctrl\", spoken: [\"Control\", \"Control\"], glyph: true },\n  alt: { mac: \"⌥\", other: \"Alt\", spoken: [\"Option\", \"Alt\"], glyph: true },\n  shift: { mac: \"⇧\", other: \"Shift\", spoken: [\"Shift\", \"Shift\"], glyph: true },\n  enter: { mac: \"↵\", other: \"Enter\", spoken: [\"Return\", \"Enter\"], glyph: true },\n  esc: { mac: \"Esc\", other: \"Esc\", spoken: [\"Escape\", \"Escape\"] },\n  tab: { mac: \"Tab\", other: \"Tab\", spoken: [\"Tab\", \"Tab\"] },\n  space: { mac: \"Space\", other: \"Space\", spoken: [\"Space\", \"Space\"] },\n  backspace: { mac: \"⌫\", other: \"Backspace\", spoken: [\"Delete\", \"Backspace\"], glyph: true },\n  delete: { mac: \"⌦\", other: \"Del\", spoken: [\"Forward delete\", \"Delete\"], glyph: true },\n  up: { mac: \"↑\", other: \"↑\", spoken: [\"Up arrow\", \"Up arrow\"], glyph: true },\n  down: { mac: \"↓\", other: \"↓\", spoken: [\"Down arrow\", \"Down arrow\"], glyph: true },\n  left: { mac: \"←\", other: \"←\", spoken: [\"Left arrow\", \"Left arrow\"], glyph: true },\n  right: { mac: \"→\", other: \"→\", spoken: [\"Right arrow\", \"Right arrow\"], glyph: true },\n  pageup: { mac: \"PgUp\", other: \"PgUp\", spoken: [\"Page up\", \"Page up\"] },\n  pagedown: { mac: \"PgDn\", other: \"PgDn\", spoken: [\"Page down\", \"Page down\"] },\n  home: { mac: \"Home\", other: \"Home\", spoken: [\"Home\", \"Home\"] },\n  end: { mac: \"End\", other: \"End\", spoken: [\"End\", \"End\"] },\n};\n\n/** Other spellings people use for the same key */\nconst ALIAS: Record<string, string> = {\n  command: \"cmd\", meta: \"cmd\", \"⌘\": \"cmd\", control: \"ctrl\", option: \"alt\", opt: \"alt\", return: \"enter\", escape: \"esc\",\n  del: \"delete\", arrowup: \"up\", arrowdown: \"down\", arrowleft: \"left\", arrowright: \"right\", pgup: \"pageup\", pgdn: \"pagedown\",\n};\n\n/** Spoken names for punctuation a screen reader might skip */\nconst SPOKEN: Record<string, string> = {\n  \".\": \"Period\", \",\": \"Comma\", \"/\": \"Slash\", \"\\\\\": \"Backslash\", \"?\": \"Question mark\", \"[\": \"Left bracket\",\n  \"]\": \"Right bracket\", \"-\": \"Minus\", \"+\": \"Plus\", \"=\": \"Equals\", \";\": \"Semicolon\", \"'\": \"Quote\", \"`\": \"Backtick\",\n};\n\nconst describe = (raw: string, os: KbdPlatform) => {\n  const key = raw.trim();\n  const id = ALIAS[key.toLowerCase()] ?? key.toLowerCase();\n  const named = NAMED[id];\n  if (named) {\n    const mac = os === \"mac\";\n    const label = mac ? named.mac : named.other;\n    return { label, spoken: named.spoken[mac ? 0 : 1], glyph: !!named.glyph && label.length === 1 };\n  }\n  // A single letter reads as a capital, like on the key itself\n  const label = key.length === 1 ? key.toUpperCase() : key;\n  return { label, spoken: SPOKEN[key] ?? label, glyph: false };\n};\n\n/* ---------- Platform ---------- */\n\nconst noop = () => () => {};\nconst isMac = () => {\n  if (typeof navigator === \"undefined\") return false;\n  const nav = navigator as Navigator & { userAgentData?: { platform?: string } };\n  return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform || nav.platform || nav.userAgent || \"\");\n};\n/** \"other\" on the server and during hydration, then the real platform – no hydration mismatch */\nconst usePlatform = (): KbdPlatform => (useSyncExternalStore(noop, isMac, () => false) ? \"mac\" : \"other\");\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n/* Colors as variables – override them on .ui-kbdc (see Theming in the docs). The chip takes its tint from the text\n   around it, so it reads on white, in dark mode and on a dark button alike */\n.ui-kbdc {\n  --ui-kbdc-bg: color-mix(in srgb, currentColor 7%, transparent);\n  --ui-kbdc-edge: color-mix(in srgb, currentColor 6%, transparent);\n  --h: 16px;\n  --pad: 4px;\n\n  display: inline-flex;\n  flex: none;\n  align-items: center;\n  justify-content: center;\n  box-sizing: border-box;\n  min-width: var(--h);\n  height: var(--h);\n  margin: 0;\n  padding: 0 var(--pad);\n  border-radius: calc(var(--aui-radius-sm, 5px) - 1px);\n  background: var(--ui-kbdc-bg);\n  box-shadow: inset 0 0 0 0.5px var(--ui-kbdc-edge);\n  font-family: var(--aui-font, \"Inter\", \"InterVariable\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  font-size: 10.5px;\n  font-weight: 500;\n  font-style: normal;\n  line-height: 1;\n  letter-spacing: 0.02em;\n  white-space: nowrap;\n  vertical-align: middle;\n  font-variant-numeric: tabular-nums;\n  -webkit-font-smoothing: antialiased;\n  user-select: none;\n}\n.ui-kbdc[data-size=\"md\"] {\n  --h: 20px;\n  --pad: 5px;\n  border-radius: var(--aui-radius-sm, 5px);\n  font-size: 12px;\n}\n\n/* A combo sits in one chip: ⌘K close together, \"Ctrl K\" with a small space – left to right even in RTL text, like\n   the keys are pressed */\n.ui-kbdc__keys {\n  display: inline-flex;\n  align-items: center;\n  gap: 1px;\n  direction: ltr;\n  unicode-bidi: isolate;\n}\n.ui-kbdc[data-os=\"other\"] .ui-kbdc__keys { gap: 3px; }\n/* ⌘ ⇧ ⌥ and arrows look small next to letters in most fonts – a touch larger */\n.ui-kbdc__key[data-glyph] { font-size: 1.1em; }\n\n/* Read by screen readers, not drawn */\n.ui-kbdc__sr {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  margin: -1px;\n  padding: 0;\n  overflow: hidden;\n  clip-path: inset(50%);\n  white-space: nowrap;\n  border: 0;\n}\n.ui-kbdc:has(.ui-kbdc__sr) { position: relative; }\n\n/* Windows high contrast: the tint is dropped, an edge keeps the chip visible */\n@media (forced-colors: active) {\n  .ui-kbdc { border: 1px solid CanvasText; }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> – the tint follows the light text */\n:where(.dark, [data-theme=\"dark\"]) .ui-kbdc {\n  --ui-kbdc-bg: color-mix(in srgb, currentColor 10%, transparent);\n  --ui-kbdc-edge: color-mix(in srgb, currentColor 8%, transparent);\n}\n`;\n"}]}