{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kbd",
  "title": "Kbd",
  "description": "A keycap that presses down when the real key does: the face drops 1px onto its base and springs back on release. Glyphs are announced by name; useKeysHeld lights up whatever a chord belongs to.",
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/kbd/kbd.tsx",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n/* ---------- Which keys are down: one shared listener for every keycap on the page ---------- */\n\n// code → the key it produced when pressed, so releasing ⇧ before \"?\" still clears \"?\".\nconst down = new Map<string, string>();\nconst listeners = new Set<() => void>();\nconst emit = () => listeners.forEach((l) => l());\n\nconst onDown = (e: KeyboardEvent) => {\n  const key = e.key.toLowerCase();\n  if (down.get(e.code) === key) return; // auto-repeat\n  down.set(e.code, key);\n  emit();\n};\nconst onUp = (e: KeyboardEvent) => {\n  // macOS drops the keyup of any key released while ⌘ is held, so letting go of ⌘ clears them all.\n  if (e.key === \"Meta\") down.clear();\n  else down.delete(e.code);\n  emit();\n};\nconst reset = () => {\n  if (!down.size) return;\n  down.clear();\n  emit();\n};\n\nfunction subscribe(listener: () => void) {\n  if (!listeners.size) {\n    window.addEventListener(\"keydown\", onDown, true);\n    window.addEventListener(\"keyup\", onUp, true);\n    window.addEventListener(\"blur\", reset);\n    document.addEventListener(\"visibilitychange\", reset);\n  }\n  listeners.add(listener);\n  return () => {\n    listeners.delete(listener);\n    if (listeners.size) return;\n    window.removeEventListener(\"keydown\", onDown, true);\n    window.removeEventListener(\"keyup\", onUp, true);\n    window.removeEventListener(\"blur\", reset);\n    document.removeEventListener(\"visibilitychange\", reset);\n    down.clear();\n  };\n}\n\n/* ---------- What each printed glyph means ---------- */\n\ntype KeySpec = { match: string[]; name: string };\n\nconst NAMED: Record<string, KeySpec> = {\n  \"⌘\": { match: [\"key:meta\"], name: \"Command\" },\n  cmd: { match: [\"key:meta\"], name: \"Command\" },\n  \"⌥\": { match: [\"key:alt\"], name: \"Option\" },\n  alt: { match: [\"key:alt\"], name: \"Alt\" },\n  opt: { match: [\"key:alt\"], name: \"Option\" },\n  \"⇧\": { match: [\"key:shift\"], name: \"Shift\" },\n  shift: { match: [\"key:shift\"], name: \"Shift\" },\n  \"⌃\": { match: [\"key:control\"], name: \"Control\" },\n  ctrl: { match: [\"key:control\"], name: \"Control\" },\n  \"↵\": { match: [\"key:enter\"], name: \"Enter\" },\n  \"⏎\": { match: [\"key:enter\"], name: \"Enter\" },\n  enter: { match: [\"key:enter\"], name: \"Enter\" },\n  return: { match: [\"key:enter\"], name: \"Return\" },\n  \"⎋\": { match: [\"key:escape\"], name: \"Escape\" },\n  esc: { match: [\"key:escape\"], name: \"Escape\" },\n  \"⌫\": { match: [\"key:backspace\"], name: \"Delete\" },\n  \"⌦\": { match: [\"key:delete\"], name: \"Forward delete\" },\n  \"⇥\": { match: [\"key:tab\"], name: \"Tab\" },\n  tab: { match: [\"key:tab\"], name: \"Tab\" },\n  space: { match: [\"code:Space\"], name: \"Space\" },\n  \"␣\": { match: [\"code:Space\"], name: \"Space\" },\n  \"↑\": { match: [\"key:arrowup\"], name: \"Up arrow\" },\n  \"↓\": { match: [\"key:arrowdown\"], name: \"Down arrow\" },\n  \"←\": { match: [\"key:arrowleft\"], name: \"Left arrow\" },\n  \"→\": { match: [\"key:arrowright\"], name: \"Right arrow\" },\n};\n\n/** Maps a printed glyph (\"⌘\", \"K\", \"/\", \"Esc\") to the keyboard events that press it and a spoken name. */\nexport function keySpec(glyph: string): KeySpec {\n  const named = NAMED[glyph] ?? NAMED[glyph.toLowerCase()];\n  if (named) return named;\n  if (/^[a-z]$/i.test(glyph)) return { match: [`code:Key${glyph.toUpperCase()}`], name: glyph.toUpperCase() };\n  if (/^\\d$/.test(glyph)) return { match: [`code:Digit${glyph}`, `code:Numpad${glyph}`], name: glyph };\n  if (/^f\\d{1,2}$/i.test(glyph)) return { match: [`key:${glyph.toLowerCase()}`], name: glyph.toUpperCase() };\n  return { match: [`key:${glyph.toLowerCase()}`], name: glyph };\n}\n\nconst never = () => () => {};\n\nconst isDown = (spec: KeySpec) =>\n  spec.match.some((m) => (m.startsWith(\"code:\") ? down.has(m.slice(5)) : [...down.values()].includes(m.slice(4))));\n\n/**\n * True while every key in `keys` is held, e.g. `useKeysHeld([\"⌘\", \"K\"])`, for lighting up the\n * row a shortcut belongs to. Shares the keycaps' single listener.\n */\nexport function useKeysHeld(keys: string[], enabled = true) {\n  const id = keys.join(\"\\u0000\");\n  return useSyncExternalStore(\n    enabled ? subscribe : never,\n    () => id.split(\"\\u0000\").every((k) => isDown(keySpec(k))),\n    () => false,\n  );\n}\n\n/* ---------- The keycap ---------- */\n\nexport type KbdProps = {\n  /** One key, e.g. `\"K\"`, `\"⌘\"`, `\"Esc\"`. Use `keys` for a chord. */\n  children?: string;\n  /** A chord, e.g. `[\"⌘\", \"K\"]`; each key is its own cap. */\n  keys?: string[];\n  /** Press the cap while the real key is held. Default true. */\n  listen?: boolean;\n  /** Force the pressed look, e.g. to replay a shortcut in a tutorial. Overrides `listen`. */\n  pressed?: boolean;\n  /** 20px (`sm`, default) or 24px (`md`) caps. */\n  size?: \"sm\" | \"md\";\n  className?: string;\n};\n\nconst SIZES = {\n  sm: \"h-5 min-w-5 px-1 text-ui-xs\",\n  md: \"h-6 min-w-6 px-1.5 text-ui-sm\",\n} as const;\n\n/**\n * A keycap that presses down when the real key does: the face drops 1px onto its base and\n * lightens, in 100ms, and springs back on release. Glyphs are announced by name (\"Command K\").\n */\nexport function Kbd({ children, keys, listen = true, pressed, size = \"sm\", className = \"\" }: KbdProps) {\n  const list = keys ?? (children !== undefined ? [children] : []);\n  if (list.length === 1) return <Cap glyph={list[0]} listen={listen} pressed={pressed} size={size} className={className} />;\n  return (\n    <kbd className={`inline-flex items-center gap-0.5 font-sans ${className}`}>\n      {list.map((glyph, i) => (\n        <Cap key={`${glyph}${i}`} glyph={glyph} listen={listen} pressed={pressed} size={size} />\n      ))}\n    </kbd>\n  );\n}\n\nfunction Cap({ glyph, listen, pressed, size, className = \"\" }: { glyph: string; listen: boolean; pressed?: boolean; size: \"sm\" | \"md\"; className?: string }) {\n  const spec = keySpec(glyph);\n  const held = useSyncExternalStore(\n    listen && pressed === undefined ? subscribe : never,\n    () => isDown(spec),\n    () => false,\n  );\n  const on = pressed ?? held;\n  const spoken = spec.name !== glyph;\n\n  return (\n    // The base is the cap's lower edge: 1px of it shows below the face at rest, and the face\n    // covers it when pressed, so the press reads as travel without moving the layout.\n    <kbd\n      data-pressed={on || undefined}\n      className={`group/cap inline-flex shrink-0 rounded-control-xs bg-line pb-px align-middle font-sans ${className}`}\n    >\n      <span\n        className={`inline-flex items-center justify-center rounded-control-xs border border-line-subtle bg-surface-raised font-medium text-text-secondary [transition:color_150ms_var(--eased-ease-standard),background-color_150ms_var(--eased-ease-standard),translate_var(--eased-duration-spring-snappy)_var(--eased-spring-snappy)] group-data-pressed/cap:translate-y-px group-data-pressed/cap:bg-surface-active group-data-pressed/cap:text-text group-data-pressed/cap:duration-(--eased-duration-instant) group-data-pressed/cap:ease-standard reduced:group-data-pressed/cap:translate-y-0 ${SIZES[size]}`}\n      >\n        {spoken ? (\n          <>\n            <span aria-hidden>{glyph}</span>\n            <span className=\"sr-only\">{spec.name}</span>\n          </>\n        ) : (\n          glyph\n        )}\n      </span>\n    </kbd>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/kbd/kbd.tsx"
    }
  ],
  "docs": "Import Kbd and useKeysHeld from components/eased/kbd/kbd. The Eased tokens were merged into your global CSS; nothing to import. Dark mode follows .dark or data-theme=\"dark\".",
  "type": "registry:ui"
}