{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"motion-kbd","type":"registry:component","title":"Kbd (Motion)","description":"Kbd with motion: its keys press in as you hold the real ones on your keyboard.","dependencies":["motion"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/motion-reduced.json","https://dashral.com/r/motion-tokens.json","https://dashral.com/r/kbd.json"],"files":[{"path":"registry/dashral/motion/kbd.tsx","type":"registry:file","target":"components/dashral/motion/kbd.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { motion } from \"motion/react\"\nimport { describeKey, kbdKeyClass, useAppleKeyboard, type KbdProps as KitKbdProps } from \"../kbd\"\nimport { cn } from \"../lib/utils\"\n\nimport { useReducedMotion } from \"../lib/motion-reduced\"\nimport { spring } from \"../lib/motion-tokens\"\n\nexport type KbdProps = KitKbdProps & {\n  /** The keys press in as the real ones are held on the keyboard. On by default. */\n  listen?: boolean\n}\n\n/** What event.key a named key is, for the ones that aren't modifiers. */\nconst eventKeys: Record<string, string> = { enter: \"Enter\", esc: \"Escape\", tab: \"Tab\", backspace: \"Backspace\", space: \" \", up: \"ArrowUp\", down: \"ArrowDown\", left: \"ArrowLeft\", right: \"ArrowRight\" }\n\n/** Whether a key of the shortcut is held, from the event's modifier flags and the other keys down. */\nfunction isHeld(key: string, apple: boolean, flags: { meta: boolean; ctrl: boolean; alt: boolean; shift: boolean }, down: Set<string>) {\n  switch (key.toLowerCase()) {\n    case \"mod\":\n      return apple ? flags.meta : flags.ctrl\n    case \"cmd\":\n      return flags.meta\n    case \"ctrl\":\n      return flags.ctrl\n    case \"alt\":\n      return flags.alt\n    case \"shift\":\n      return flags.shift\n    default:\n      return down.has((eventKeys[key.toLowerCase()] ?? key).toLowerCase())\n  }\n}\n\n/**\n * The kit's Kbd with motion: the same keys, sizes and props, and the keys answer the keyboard. Hold ⌘ and K and those\n * caps press in, springing back up as you let go, so a shortcut shown on the page shows itself working. listen={false}\n * keeps them still. With reduced motion they only change color.\n */\nexport function Kbd({ keys, children, size = \"default\", listen = true, className, ...props }: KbdProps) {\n  const apple = useAppleKeyboard()\n  const reduced = useReducedMotion()\n  const [held, setHeld] = React.useState<boolean[]>([])\n\n  React.useEffect(() => {\n    if (!listen || !keys) return\n    const down = new Set<string>()\n    const update = (event: KeyboardEvent) => {\n      const flags = { meta: event.metaKey, ctrl: event.ctrlKey, alt: event.altKey, shift: event.shiftKey }\n      const next = keys.map((key) => isHeld(key, apple, flags, down))\n      setHeld((was) => (was.length === next.length && was.every((v, i) => v === next[i]) ? was : next))\n    }\n    const press = (event: KeyboardEvent) => {\n      down.add(event.key.toLowerCase())\n      update(event)\n    }\n    const release = (event: KeyboardEvent) => {\n      down.delete(event.key.toLowerCase())\n      // With ⌘ held, the browser never sends the other key's keyup: letting go of ⌘ lets go of them all.\n      if (event.key === \"Meta\") down.clear()\n      update(event)\n    }\n    const reset = () => {\n      down.clear()\n      setHeld([])\n    }\n    window.addEventListener(\"keydown\", press)\n    window.addEventListener(\"keyup\", release)\n    window.addEventListener(\"blur\", reset)\n    return () => {\n      window.removeEventListener(\"keydown\", press)\n      window.removeEventListener(\"keyup\", release)\n      window.removeEventListener(\"blur\", reset)\n    }\n  }, [listen, keys, apple])\n\n  const cap = (label: React.ReactNode, pressed: boolean, i: number) => (\n    <motion.kbd\n      key={i}\n      aria-hidden={keys ? true : undefined}\n      data-pressed={pressed || undefined}\n      className={cn(kbdKeyClass(size), \"transition-colors data-pressed:border-ring/50 data-pressed:bg-accent data-pressed:text-foreground\")}\n      // Pressed, it sinks a little: down by its bottom edge, a touch smaller.\n      animate={pressed && !reduced ? { y: 1.5, scale: 0.94 } : { y: 0, scale: 1 }}\n      transition={spring.press}\n    >\n      {label}\n    </motion.kbd>\n  )\n\n  if (!keys)\n    return (\n      <kbd data-slot=\"kbd\" className={cn(\"inline-flex\", className)} {...props}>\n        {cap(children, false, 0)}\n      </kbd>\n    )\n  const described = keys.map((key) => describeKey(key, apple))\n  return (\n    // Nested kbd for a combination, as HTML has it; read out as words (\"Command K\"), not symbols.\n    <kbd data-slot=\"kbd\" aria-label={described.map((key) => key.name).join(\" \")} className={cn(\"inline-flex items-center gap-1\", className)} {...props}>\n      {described.map((key, i) => cap(key.label, held[i] ?? false, i))}\n    </kbd>\n  )\n}\n"}]}