{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"kbd","type":"registry:component","title":"Kbd","description":"Keyboard key chips and key combos that render ⌘ or Ctrl for the visitor's platform without hydration mismatch, in two sizes, with a spring dip when pressed and an optional listen mode that animates each key as the real one is held.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/kbd.tsx","type":"registry:component","target":"@components/motion/kbd.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/motion/kbd\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { Fragment, type ReactNode, useEffect, useState } from \"react\";\nimport { SPRING_PRESS } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\n/* -------------------------------------------------------------------------- */\n/*                                  Platform                                  */\n/* -------------------------------------------------------------------------- */\n\nfunction detectApple() {\n  const nav = navigator as Navigator & {\n    userAgentData?: { platform?: string };\n  };\n  const platform = nav.userAgentData?.platform ?? nav.platform ?? \"\";\n  return /mac|iphone|ipad|ipod/i.test(platform);\n}\n\n/**\n * True on Apple platforms. Always `true` on the server and the first client\n * render (so markup matches), then corrected after mount — `Mod` therefore\n * renders ⌘ first and swaps to Ctrl on other platforms.\n */\nexport function useIsApple() {\n  const [apple, setApple] = useState(true);\n  useEffect(() => setApple(detectApple()), []);\n  return apple;\n}\n\ntype KeyLabel = { glyph: string; name: string };\n\n/** Display glyph + spoken name for a key token like \"mod\", \"shift\", \"k\". */\nexport function keyLabel(key: string, apple: boolean): KeyLabel {\n  const k = key.toLowerCase();\n  switch (k) {\n    case \"mod\":\n      return apple\n        ? { glyph: \"⌘\", name: \"Command\" }\n        : { glyph: \"Ctrl\", name: \"Control\" };\n    case \"cmd\":\n    case \"command\":\n    case \"meta\":\n      return apple\n        ? { glyph: \"⌘\", name: \"Command\" }\n        : { glyph: \"Win\", name: \"Windows\" };\n    case \"ctrl\":\n    case \"control\":\n      return apple\n        ? { glyph: \"⌃\", name: \"Control\" }\n        : { glyph: \"Ctrl\", name: \"Control\" };\n    case \"shift\":\n      return apple\n        ? { glyph: \"⇧\", name: \"Shift\" }\n        : { glyph: \"Shift\", name: \"Shift\" };\n    case \"alt\":\n    case \"option\":\n      return apple\n        ? { glyph: \"⌥\", name: \"Option\" }\n        : { glyph: \"Alt\", name: \"Alt\" };\n    case \"enter\":\n    case \"return\":\n      return { glyph: \"↵\", name: \"Enter\" };\n    case \"esc\":\n    case \"escape\":\n      return { glyph: \"Esc\", name: \"Escape\" };\n    case \"backspace\":\n      return { glyph: \"⌫\", name: \"Backspace\" };\n    case \"delete\":\n      return { glyph: \"⌦\", name: \"Delete\" };\n    case \"tab\":\n      return { glyph: \"⇥\", name: \"Tab\" };\n    case \"space\":\n      return { glyph: \"Space\", name: \"Space\" };\n    case \"up\":\n    case \"arrowup\":\n      return { glyph: \"↑\", name: \"Up arrow\" };\n    case \"down\":\n    case \"arrowdown\":\n      return { glyph: \"↓\", name: \"Down arrow\" };\n    case \"left\":\n    case \"arrowleft\":\n      return { glyph: \"←\", name: \"Left arrow\" };\n    case \"right\":\n    case \"arrowright\":\n      return { glyph: \"→\", name: \"Right arrow\" };\n    default:\n      return key.length === 1\n        ? { glyph: key.toUpperCase(), name: key.toUpperCase() }\n        : { glyph: key, name: key };\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                               Key listening                                */\n/* -------------------------------------------------------------------------- */\n\n/** Does this keyboard event's key correspond to the token? */\nfunction eventMatches(token: string, event: KeyboardEvent, apple: boolean) {\n  const k = token.toLowerCase();\n  const key = event.key.toLowerCase();\n  switch (k) {\n    case \"mod\":\n      return key === (apple ? \"meta\" : \"control\");\n    case \"cmd\":\n    case \"command\":\n    case \"meta\":\n      return key === \"meta\";\n    case \"ctrl\":\n    case \"control\":\n      return key === \"control\";\n    case \"alt\":\n    case \"option\":\n      return key === \"alt\";\n    case \"esc\":\n      return key === \"escape\";\n    case \"return\":\n      return key === \"enter\";\n    case \"space\":\n      return event.code === \"Space\";\n    case \"up\":\n    case \"down\":\n    case \"left\":\n    case \"right\":\n      return key === `arrow${k}`;\n    default:\n      // Letters and digits by physical key, so Shift/Option variants\n      // (\"P\" with Shift, \"π\" with Option) still match.\n      if (/^[a-z]$/.test(k)) return event.code === `Key${k.toUpperCase()}`;\n      if (/^[0-9]$/.test(k)) return event.code === `Digit${k}`;\n      return key === k;\n  }\n}\n\nconst MODIFIERS = new Set([\"meta\", \"control\", \"shift\", \"alt\"]);\n\n/**\n * Tracks which of `keys` are physically held. Listens on window without\n * preventing default, so it never steals the shortcut from the page.\n */\nexport function useKeysPressed(keys: string[], enabled = true) {\n  const apple = useIsApple();\n  const [held, setHeld] = useState<boolean[]>(() => keys.map(() => false));\n  const signature = keys.join(\"+\");\n\n  useEffect(() => {\n    const tokens = signature.split(\"+\");\n    setHeld(tokens.map(() => false));\n    if (!enabled) return;\n    const down = new Set<number>();\n    const sync = () => setHeld(tokens.map((_, i) => down.has(i)));\n\n    const onDown = (event: KeyboardEvent) => {\n      let changed = false;\n      tokens.forEach((token, i) => {\n        if (!down.has(i) && eventMatches(token, event, apple)) {\n          down.add(i);\n          changed = true;\n        }\n      });\n      if (changed) sync();\n    };\n    const onUp = (event: KeyboardEvent) => {\n      // macOS swallows keyup for other keys while ⌘ is held, so releasing a\n      // modifier releases everything.\n      if (MODIFIERS.has(event.key.toLowerCase()) && apple) {\n        down.clear();\n        sync();\n        return;\n      }\n      let changed = false;\n      tokens.forEach((token, i) => {\n        if (down.has(i) && eventMatches(token, event, apple)) {\n          down.delete(i);\n          changed = true;\n        }\n      });\n      if (changed) sync();\n    };\n    const reset = () => {\n      down.clear();\n      sync();\n    };\n    window.addEventListener(\"keydown\", onDown);\n    window.addEventListener(\"keyup\", onUp);\n    window.addEventListener(\"blur\", reset);\n    return () => {\n      window.removeEventListener(\"keydown\", onDown);\n      window.removeEventListener(\"keyup\", onUp);\n      window.removeEventListener(\"blur\", reset);\n    };\n  }, [signature, enabled, apple]);\n\n  return held;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Kbd                                     */\n/* -------------------------------------------------------------------------- */\n\ntype KbdSize = \"sm\" | \"md\";\n\nconst SIZE: Record<KbdSize, string> = {\n  sm: \"h-5 min-w-5 px-1 text-[11px] rounded-[5px] border-b-[2px]\",\n  md: \"h-6 min-w-6 px-1.5 text-xs rounded-md border-b-[3px]\",\n};\n\n// How far the key drops when pressed, in px — matches the thicker bottom edge\n// so the cap visibly sinks into its base.\nconst DIP: Record<KbdSize, number> = { sm: 1.5, md: 2 };\n\nexport interface KbdProps {\n  /** A key token (\"mod\", \"shift\", \"k\", \"enter\"…) or any custom content. */\n  children: ReactNode;\n  size?: KbdSize;\n  /** Show the key pressed (spring dip). */\n  pressed?: boolean;\n  /** Dip when the real key is held. Needs `children` to be a key token string. */\n  listen?: boolean;\n  className?: string;\n}\n\n/**\n * A single keyboard key. String children are treated as key tokens, so\n * `<Kbd>mod</Kbd>` renders ⌘ on Apple platforms and Ctrl elsewhere.\n */\nexport function Kbd({\n  children,\n  size = \"md\",\n  pressed = false,\n  listen = false,\n  className,\n}: KbdProps) {\n  const apple = useIsApple();\n  const token = typeof children === \"string\" ? children : null;\n  const [held] = useKeysPressed(token ? [token] : [], listen && token !== null);\n  const label = token ? keyLabel(token, apple) : null;\n\n  return (\n    <KeyCap\n      size={size}\n      pressed={pressed || Boolean(held)}\n      title={label?.name}\n      className={className}\n    >\n      {label ? label.glyph : children}\n    </KeyCap>\n  );\n}\n\nfunction KeyCap({\n  children,\n  size,\n  pressed,\n  title,\n  className,\n}: {\n  children: ReactNode;\n  size: KbdSize;\n  pressed: boolean;\n  title?: string;\n  className?: string;\n}) {\n  const reduce = useReducedMotion() ?? false;\n  return (\n    <motion.kbd\n      title={title}\n      data-pressed={pressed ? \"\" : undefined}\n      initial={false}\n      animate={\n        reduce\n          ? { opacity: pressed ? 0.7 : 1 }\n          : { y: pressed ? DIP[size] : 0, scale: pressed ? 0.96 : 1 }\n      }\n      transition={reduce ? { duration: 0.1 } : SPRING_PRESS}\n      className={cn(\n        \"inline-flex select-none items-center justify-center border border-border border-b-border-strong bg-muted font-sans font-medium leading-none text-muted-foreground\",\n        \"data-[pressed]:border-b-border data-[pressed]:bg-foreground/[0.08] data-[pressed]:text-foreground\",\n        SIZE[size],\n        className,\n      )}\n    >\n      {children}\n    </motion.kbd>\n  );\n}\n\nexport interface KbdComboProps {\n  /** Key tokens, e.g. [\"mod\", \"shift\", \"p\"]. */\n  keys: string[];\n  size?: KbdSize;\n  /** Show every key pressed. */\n  pressed?: boolean;\n  /** Dip each key while the real key is held. */\n  listen?: boolean;\n  /** Text between keys. Default none on Apple, \"+\" elsewhere. */\n  separator?: ReactNode;\n  className?: string;\n}\n\n/**\n * A key combination: one chip per key, platform-aware, with a spoken label\n * (\"Command + Shift + P\") for assistive tech.\n */\nexport function KbdCombo({\n  keys,\n  size = \"md\",\n  pressed = false,\n  listen = false,\n  separator,\n  className,\n}: KbdComboProps) {\n  const apple = useIsApple();\n  const held = useKeysPressed(keys, listen);\n  const labels = keys.map((key) => keyLabel(key, apple));\n  const sep = separator === undefined ? (apple ? null : \"+\") : separator;\n\n  return (\n    <kbd\n      className={cn(\n        \"inline-flex items-center gap-1 font-sans text-muted-foreground\",\n        className,\n      )}\n    >\n      <span className=\"sr-only\">{labels.map((l) => l.name).join(\" + \")}</span>\n      <span aria-hidden=\"true\" className=\"inline-flex items-center gap-1\">\n        {labels.map((label, i) => (\n          <Fragment key={keys[i]}>\n            {i > 0 && sep !== null ? (\n              <span className=\"text-[10px] text-muted-foreground/70\">{sep}</span>\n            ) : null}\n            <KeyCap size={size} pressed={pressed || Boolean(held[i])}>\n              {label.glyph}\n            </KeyCap>\n          </Fragment>\n        ))}\n      </span>\n    </kbd>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}