{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"kbd","type":"registry:ui","title":"Kbd","description":"Keyboard keys drawn as 3D keycaps that physically sink when the matching key is pressed, plus a group for shortcuts.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/kbd.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { Children, Fragment, useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type KbdSize = \"sm\" | \"md\" | \"lg\";\n\n// Symbols and names people write on keycaps, mapped to KeyboardEvent.key.\nconst ALIASES: Record<string, string> = {\n  \"⌘\": \"Meta\", cmd: \"Meta\", command: \"Meta\", meta: \"Meta\", win: \"Meta\",\n  \"⌃\": \"Control\", ctrl: \"Control\", control: \"Control\",\n  \"⌥\": \"Alt\", opt: \"Alt\", option: \"Alt\", alt: \"Alt\",\n  \"⇧\": \"Shift\", shift: \"Shift\",\n  \"↵\": \"Enter\", \"⏎\": \"Enter\", enter: \"Enter\", return: \"Enter\",\n  \"⎋\": \"Escape\", esc: \"Escape\", escape: \"Escape\",\n  \"⌫\": \"Backspace\", backspace: \"Backspace\", \"⌦\": \"Delete\", del: \"Delete\",\n  \"⇥\": \"Tab\", tab: \"Tab\",\n  \"␣\": \" \", space: \" \",\n  \"↑\": \"ArrowUp\", \"↓\": \"ArrowDown\", \"←\": \"ArrowLeft\", \"→\": \"ArrowRight\",\n};\n\nfunction toKey(label: string) {\n  const l = label.trim();\n  if (l.toLowerCase() === \"mod\") return /Mac|iPhone|iPad/.test(navigator.platform) ? \"Meta\" : \"Control\";\n  return (ALIASES[l] ?? ALIASES[l.toLowerCase()] ?? l).toLowerCase();\n}\n\nconst sizes: Record<KbdSize, string> = {\n  sm: \"h-5 min-w-5 rounded-[5px] px-1 text-[10px] [--depth:2px]\",\n  md: \"h-7 min-w-7 rounded-md px-1.5 text-xs [--depth:3px]\",\n  lg: \"h-10 min-w-10 rounded-lg px-2.5 text-sm [--depth:4px]\",\n};\n\nexport interface KbdProps extends HTMLAttributes<HTMLElement> {\n  size?: KbdSize;\n  /** Animate a press when the matching physical key goes down. */\n  listen?: boolean;\n  /** KeyboardEvent.key to react to. Defaults to the text content (\"⌘\", \"Shift\", \"K\", \"Mod\"...). */\n  match?: string;\n  /** Force the pressed look (controlled). */\n  pressed?: boolean;\n}\n\nexport function Kbd({ size = \"md\", listen = true, match, pressed, className, children, ...props }: KbdProps) {\n  const [down, setDown] = useState(false);\n  const label = match ?? (typeof children === \"string\" ? children : undefined);\n\n  useEffect(() => {\n    if (!listen || !label) return;\n    const key = toKey(label);\n    // Passive observers only: never preventDefault, so page shortcuts keep working.\n    const on = (e: KeyboardEvent) => e.key.toLowerCase() === key && setDown(true);\n    const off = (e: KeyboardEvent) => e.key.toLowerCase() === key && setDown(false);\n    const reset = () => setDown(false);\n    window.addEventListener(\"keydown\", on);\n    window.addEventListener(\"keyup\", off);\n    window.addEventListener(\"blur\", reset);\n    return () => {\n      window.removeEventListener(\"keydown\", on);\n      window.removeEventListener(\"keyup\", off);\n      window.removeEventListener(\"blur\", reset);\n    };\n  }, [listen, label]);\n\n  const isDown = pressed ?? down;\n\n  return (\n    <kbd\n      {...props}\n      data-pressed={isDown || undefined}\n      className={cn(\n        \"relative inline-flex select-none items-center justify-center border border-border bg-linear-to-b from-card to-muted font-sans font-medium text-foreground\",\n        // The key's \"side\" is a hard shadow; pressing sinks the cap into it.\n        \"[--side:color-mix(in_oklab,var(--muted-foreground)_40%,var(--background))] shadow-[0_var(--depth)_0_0_var(--side),0_calc(var(--depth)+2px)_6px_-2px_rgb(0_0_0/0.35),inset_0_1px_0_rgb(255_255_255/0.08)]\",\n        \"transition-[translate,box-shadow,background-color,border-color] duration-100 ease-out motion-reduce:transition-none\",\n        \"data-pressed:translate-y-(--depth) data-pressed:border-primary/60 data-pressed:bg-muted data-pressed:shadow-[0_0_0_0_var(--side),0_0_0_3px_color-mix(in_oklab,var(--primary)_22%,transparent),inset_0_1px_2px_rgb(0_0_0/0.25)]\",\n        sizes[size],\n        className,\n      )}\n    >\n      {children}\n    </kbd>\n  );\n}\n\nexport interface KbdGroupProps extends HTMLAttributes<HTMLSpanElement> {\n  /** Rendered between keys, e.g. \"+\" or \"then\". */\n  separator?: ReactNode;\n}\n\nexport function KbdGroup({ separator, className, children, ...props }: KbdGroupProps) {\n  const keys = Children.toArray(children);\n  return (\n    <span {...props} className={cn(\"inline-flex items-center gap-1.5\", className)}>\n      {keys.map((k, i) => (\n        <Fragment key={i}>\n          {i > 0 && separator != null && (\n            <span aria-hidden className=\"text-xs text-muted-foreground\">\n              {separator}\n            </span>\n          )}\n          {k}\n        </Fragment>\n      ))}\n    </span>\n  );\n}\n"}]}