{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kbd-key",
  "title": "Kbd Key",
  "description": "A 3D keycap that physically depresses when the real key is pressed, with combo shortcuts that pulse and fire when every key is held at once",
  "dependencies": [
    "framer-motion"
  ],
  "files": [
    {
      "path": "app/registry/kbd-key/kbd-key.tsx",
      "content": "/**\n * Spectrum UI — KbdKey & KbdCombo\n *\n * A semantic <kbd> element rendered as a 3D keycap that physically depresses\n * when the real key is pressed — via a global keydown listener or a pointer\n * tap. Press-down is instant (keys feel mechanical) and release springs back\n * with a tiny overshoot. KbdCombo strings caps together (\"meta+k\"); when\n * every key is held at once the whole combo pulses and onTrigger fires.\n * Honors prefers-reduced-motion.\n *\n * Dependencies: framer-motion, @/lib/utils\n *\n * @example\n * <KbdKey>K</KbdKey>\n * <KbdCombo keys=\"meta+k\" onTrigger={() => openPalette()} />\n */\n\n\"use client\"\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\"\nimport { motion, useAnimationControls, useReducedMotion } from \"framer-motion\"\nimport { cn } from \"@/lib/utils\"\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface KbdKeyProps {\n  /** Cap legend, e.g. \"K\", \"⌘\" or \"esc\" */\n  children: React.ReactNode\n  /**\n   * Key to match against KeyboardEvent.key, case-insensitively. Friendly\n   * names are supported: \"meta\"/\"cmd\", \"ctrl\", \"shift\", \"alt\"/\"option\",\n   * \"enter\", \"escape\"/\"esc\", \"space\", \"up\"/\"down\"/\"left\"/\"right\". Derived\n   * from children when it is a plain string; symbol legends like \"⌘\" need\n   * an explicit keyName\n   */\n  keyName?: string\n  /** Depress the cap while the real key is held (window listener). Default true */\n  listen?: boolean\n  /** Fires once per press — on a matching keydown or a pointer tap. Also wraps the cap in a button */\n  onPress?: () => void\n  /** Visual size of the cap. Default \"md\" */\n  size?: \"sm\" | \"md\"\n  className?: string\n}\n\nexport interface KbdComboProps {\n  /** \"+\"-separated keys, e.g. \"meta+k\" or \"shift+?\" */\n  keys: string\n  /** Depress each cap while its real key is held. Default true */\n  listen?: boolean\n  /** Fires once each time every key in the combo is held down simultaneously */\n  onTrigger?: () => void\n  /** Visual size of the caps. Default \"md\" */\n  size?: \"sm\" | \"md\"\n  className?: string\n}\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\n/** How far the cap travels down while pressed, in px (matches the depth edge) */\nconst PRESS_DEPTH = 2\n\n/** Press-down is near-instant — real keys don't ease in */\nconst PRESS_TRANSITION = { duration: 0.08, ease: \"easeOut\" } as const\n\n/** Release springs back with a tiny (~0.5px) overshoot before settling */\nconst RELEASE_SPRING = { type: \"spring\", stiffness: 500, damping: 18 } as const\n\n/** Whole-combo pulse when every key is down at once */\nconst PULSE_SCALE = [1, 1.06, 1]\nconst PULSE_TRANSITION = { duration: 0.25, ease: \"easeOut\" } as const\n\n/** Friendly key names → KeyboardEvent.key values */\nconst KEY_ALIASES: Record<string, string> = {\n  meta: \"Meta\",\n  cmd: \"Meta\",\n  command: \"Meta\",\n  ctrl: \"Control\",\n  control: \"Control\",\n  shift: \"Shift\",\n  alt: \"Alt\",\n  option: \"Alt\",\n  enter: \"Enter\",\n  return: \"Enter\",\n  escape: \"Escape\",\n  esc: \"Escape\",\n  space: \" \",\n  up: \"ArrowUp\",\n  down: \"ArrowDown\",\n  left: \"ArrowLeft\",\n  right: \"ArrowRight\",\n}\n\n/** KeyboardEvent.key values → pretty cap legends (used by KbdCombo) */\nconst KEY_GLYPHS: Record<string, string> = {\n  Meta: \"⌘\",\n  Shift: \"⇧\",\n  Alt: \"⌥\",\n  Control: \"⌃\",\n  Enter: \"↵\",\n  \" \": \"␣\",\n  Escape: \"esc\",\n  ArrowUp: \"↑\",\n  ArrowDown: \"↓\",\n  ArrowLeft: \"←\",\n  ArrowRight: \"→\",\n}\n\n/** Lowercased KeyboardEvent.key values of the modifier keys */\nconst MODIFIERS = new Set([\"meta\", \"control\", \"shift\", \"alt\"])\n\nconst SIZES = {\n  sm: \"h-6 min-w-[24px] px-1.5 text-[10px]\",\n  md: \"h-7 min-w-[28px] px-2 text-xs\",\n} as const\n\n// Solid depth edge below the cap; hex = neutral-300 / neutral-600\nconst EDGE_SHADOW = \"shadow-[0_2px_0_0_#d4d4d4] dark:shadow-[0_2px_0_0_#525252]\"\nconst EDGE_SHADOW_PRESSED =\n  \"shadow-[0_0_0_0_#d4d4d4] dark:shadow-[0_0_0_0_#525252]\"\n\n/** Resolves a friendly key name (\"cmd\", \"esc\", \"k\") to its KeyboardEvent.key value */\nfunction toEventKey(name: string) {\n  return KEY_ALIASES[name.toLowerCase()] ?? name\n}\n\n/** Pretty legend for a key name — \"meta\" → \"⌘\", \"k\" → \"K\" */\nfunction toGlyph(name: string) {\n  const glyph = KEY_GLYPHS[toEventKey(name)]\n  return glyph ?? (name.length === 1 ? name.toUpperCase() : name)\n}\n\n// ─── KbdKey ──────────────────────────────────────────────────────────────────\n\nexport function KbdKey({\n  children,\n  keyName,\n  listen = true,\n  onPress,\n  size = \"md\",\n  className,\n}: KbdKeyProps) {\n  const shouldReduceMotion = useReducedMotion()\n  const [pressed, setPressed] = useState(false)\n  // Mirrors `pressed` so press/release stay idempotent without re-creating\n  // the window listeners on every state change\n  const pressedRef = useRef(false)\n  const onPressRef = useRef(onPress)\n  onPressRef.current = onPress\n  // Physical key (event.code) that pressed the cap — releases it even when\n  // dropping Shift changed event.key in between (a \"?\" keyup arrives as \"/\")\n  const pressedCodeRef = useRef<string | null>(null)\n\n  const resolvedKeyName =\n    keyName ??\n    (typeof children === \"string\" ? children.toLowerCase() : undefined)\n\n  const press = useCallback(() => {\n    if (pressedRef.current) return\n    pressedRef.current = true\n    setPressed(true)\n    onPressRef.current?.()\n  }, [])\n\n  const release = useCallback(() => {\n    if (!pressedRef.current) return\n    pressedRef.current = false\n    pressedCodeRef.current = null\n    setPressed(false)\n  }, [])\n\n  useEffect(() => {\n    if (!listen || !resolvedKeyName) return\n\n    const targetKey = toEventKey(resolvedKeyName).toLowerCase()\n    const targetIsModifier = MODIFIERS.has(targetKey)\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.repeat) return // held keys auto-repeat; the cap is already down\n      if (event.key.toLowerCase() !== targetKey) return\n      pressedCodeRef.current = event.code\n      press()\n    }\n\n    const handleKeyUp = (event: KeyboardEvent) => {\n      const releasedMatchingCode =\n        pressedCodeRef.current !== null && event.code === pressedCodeRef.current\n      // macOS suppresses keyup for non-modifier keys while ⌘ is held, so\n      // releasing ⌘ also releases this cap\n      const metaSwallowedKeyUp =\n        event.key.toLowerCase() === \"meta\" && !targetIsModifier\n      if (\n        event.key.toLowerCase() === targetKey ||\n        releasedMatchingCode ||\n        metaSwallowedKeyUp\n      ) {\n        release()\n      }\n    }\n\n    // Don't leave the cap stuck down when the tab loses focus mid-press\n    const handleBlur = () => release()\n\n    window.addEventListener(\"keydown\", handleKeyDown)\n    window.addEventListener(\"keyup\", handleKeyUp)\n    window.addEventListener(\"blur\", handleBlur)\n    return () => {\n      window.removeEventListener(\"keydown\", handleKeyDown)\n      window.removeEventListener(\"keyup\", handleKeyUp)\n      window.removeEventListener(\"blur-sm\", handleBlur)\n    }\n  }, [listen, resolvedKeyName, press, release])\n\n  const cap = (\n    <motion.kbd\n      initial={false}\n      animate={{ y: pressed ? PRESS_DEPTH : 0 }}\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : pressed\n            ? PRESS_TRANSITION\n            : RELEASE_SPRING\n      }\n      onPointerDown={(event) => {\n        if (event.button !== 0) return\n        press()\n      }}\n      onPointerUp={release}\n      onPointerLeave={release}\n      onPointerCancel={release}\n      className={cn(\n        \"inline-flex select-none items-center justify-center rounded-md border font-mono\",\n        \"border-neutral-200 text-neutral-700 dark:border-neutral-700 dark:text-neutral-200\",\n        pressed\n          ? cn(\"bg-neutral-100 dark:bg-neutral-700\", EDGE_SHADOW_PRESSED)\n          : cn(\"bg-neutral-50 dark:bg-neutral-800\", EDGE_SHADOW),\n        !shouldReduceMotion &&\n          \"transition-[background-color,box-shadow] duration-100 ease-out\",\n        SIZES[size],\n        className,\n      )}\n    >\n      {children}\n    </motion.kbd>\n  )\n\n  // Decorative caps stay non-interactive; a cap with an action is a button\n  if (!onPress) return cap\n\n  const label =\n    typeof children === \"string\" ? children : (resolvedKeyName ?? \"key\")\n\n  return (\n    <button\n      type=\"button\"\n      aria-label={`Press ${label}`}\n      onKeyDown={(event) => {\n        if (event.repeat) return\n        if (event.key === \"Enter\" || event.key === \" \") press()\n      }}\n      onKeyUp={(event) => {\n        if (event.key === \"Enter\" || event.key === \" \") release()\n      }}\n      className=\"inline-flex rounded-md focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-neutral-950 dark:focus-visible:ring-neutral-300\"\n    >\n      {cap}\n    </button>\n  )\n}\n\n// ─── KbdCombo ────────────────────────────────────────────────────────────────\n\nexport function KbdCombo({\n  keys,\n  listen = true,\n  onTrigger,\n  size = \"md\",\n  className,\n}: KbdComboProps) {\n  const shouldReduceMotion = useReducedMotion()\n  const controls = useAnimationControls()\n  const onTriggerRef = useRef(onTrigger)\n  onTriggerRef.current = onTrigger\n\n  const keyNames = keys\n    .split(\"+\")\n    .map((name) => name.trim())\n    .filter(Boolean)\n\n  useEffect(() => {\n    if (!listen) return\n\n    const targets = keys\n      .split(\"+\")\n      .map((name) => name.trim())\n      .filter(Boolean)\n      .map((name) => toEventKey(name).toLowerCase())\n    if (targets.length === 0) return\n\n    const down = new Set<string>()\n    // event.code of each physical key currently holding a target down, so a\n    // shift-shifted keyup (e.g. \"?\" released as \"/\") still clears its key\n    const codeToKey = new Map<string, string>()\n    let fired = false\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.repeat) return\n      const key = event.key.toLowerCase()\n      if (!targets.includes(key)) return\n      down.add(key)\n      codeToKey.set(event.code, key)\n      if (fired || !targets.every((target) => down.has(target))) return\n      fired = true\n      onTriggerRef.current?.()\n      if (!shouldReduceMotion) {\n        controls.start({ scale: PULSE_SCALE, transition: PULSE_TRANSITION })\n      }\n    }\n\n    const handleKeyUp = (event: KeyboardEvent) => {\n      const key = event.key.toLowerCase()\n      if (key === \"meta\") {\n        // macOS swallows non-modifier keyups while ⌘ is held; treat them as\n        // released the moment ⌘ comes up\n        down.forEach((held) => {\n          if (!MODIFIERS.has(held)) down.delete(held)\n        })\n        codeToKey.clear()\n      }\n      const mapped = codeToKey.get(event.code)\n      if (mapped) {\n        down.delete(mapped)\n        codeToKey.delete(event.code)\n      }\n      down.delete(key)\n      if (!targets.every((target) => down.has(target))) fired = false\n    }\n\n    const handleBlur = () => {\n      down.clear()\n      codeToKey.clear()\n      fired = false\n    }\n\n    window.addEventListener(\"keydown\", handleKeyDown)\n    window.addEventListener(\"keyup\", handleKeyUp)\n    window.addEventListener(\"blur\", handleBlur)\n    return () => {\n      window.removeEventListener(\"keydown\", handleKeyDown)\n      window.removeEventListener(\"keyup\", handleKeyUp)\n      window.removeEventListener(\"blur-sm\", handleBlur)\n    }\n  }, [keys, listen, shouldReduceMotion, controls])\n\n  return (\n    <motion.span\n      animate={controls}\n      className={cn(\"inline-flex items-center gap-1\", className)}\n    >\n      {keyNames.map((name, index) => (\n        <React.Fragment key={`${name}-${index}`}>\n          {index > 0 && (\n            <span\n              aria-hidden=\"true\"\n              className=\"select-none text-[10px] text-neutral-400\"\n            >\n              +\n            </span>\n          )}\n          <KbdKey keyName={name} listen={listen} size={size}>\n            {toGlyph(name)}\n          </KbdKey>\n        </React.Fragment>\n      ))}\n    </motion.span>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/spectrumui/kbd-key.tsx"
    }
  ],
  "type": "registry:component"
}
