shadimage
nexus-labs
componentmac-keyboardsource: ok · hit

Mac Keyboard

Fully interactive Mac-style keyboard with Web Audio synthesised click sounds, Framer Motion keycap animations, and real-time physical keyboard highlighting.

npx shadcn add "https://nexus-ui.com/r/mac-keyboard.json"
free

Where it goes

kanban

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationframer-motion

Registry dependencies

https://www.nexus-ui.com/api/registry/nexus-fontutils

Files6 · source cached 10/8/2026, 12:30:47 PM

next-ui/keyboard/index.ts
export { MacKeyboard, usePhysicalKeyboard } from "./keyboard";
export type { KeyboardProps } from "./keyboard";
export { useKeyboardSound } from "./use-keyboard-sound";
export type { SoundType } from "./use-keyboard-sound";
export type { KeyDef, KeyKind, SoundVariant } from "./keyboard-layout";
export { MAC_ROWS, COMPACT_ROWS } from "./keyboard-layout";
next-ui/keyboard/keyboard-key.tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion, type Variants } from "framer-motion";
import { cn } from "@/lib/utils";
import type { KeyDef } from "./keyboard-layout";

export interface KeyboardKeyProps {
  keyDef: KeyDef;
  isActive: boolean;
  isFnRow?: boolean;
  theme: "dark" | "light";
  keyClassName?: string;
  onPress: (key: KeyDef) => void;
}

export const KeyboardKey = React.memo(function KeyboardKey({
  keyDef,
  isActive,
  isFnRow = false,
  theme,
  keyClassName,
  onPress,
}: KeyboardKeyProps) {
  const reduce = useReducedMotion();
  const isDark = theme === "dark";

  const isSpace    = keyDef.kind === "space";
  const isModifier = keyDef.kind === "modifier" || keyDef.kind === "fn" || keyDef.kind === "nav";
  const isEnter    = keyDef.kind === "enter";
  const isDelete   = keyDef.kind === "delete";

  // ── Visual classes ────────────────────────────────────────────
  const baseKey = cn(
    // layout
    "relative select-none cursor-pointer outline-none",
    "flex items-end justify-center pb-1.5",
    isFnRow ? "h-7 rounded-[4px]" : "h-10 rounded-[6px]",
    // typography
    "font-[-apple-system,BlinkMacSystemFont,'SF_Pro_Display',system-ui]",
    isSpace ? "text-[0px]"
    : isFnRow ? "text-[9px] font-medium tracking-tight"
    : isModifier ? "text-[10px] font-medium tracking-tight leading-none"
    : "text-[13px] font-medium tracking-tight",
    // base colors — overridden by active/pressed variants below
    isDark
      ? "text-neutral-200 bg-gradient-to-b from-[#3e3e42] to-[#2c2c30]"
      : "text-neutral-700 bg-gradient-to-b from-[#f5f5f7] to-[#e2e2e5]",
    // border / depth shadow that creates the 3-D "key face" look
    isDark
      ? "shadow-[0_1px_0_0_#000,0_2px_4px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.07)]"
      : "shadow-[0_1px_0_0_#9e9e9e,0_2px_4px_rgba(0,0,0,0.14),inset_0_1px_0_rgba(255,255,255,0.9)]",
    // active highlight
    isActive && (
      isDark
        ? "from-[#0ea5e9] to-[#0284c7] text-white shadow-[0_1px_0_0_#0369a1,0_2px_8px_rgba(14,165,233,0.45),inset_0_1px_0_rgba(255,255,255,0.2)]"
        : "from-[#0ea5e9] to-[#0284c7] text-white shadow-[0_1px_0_0_#0369a1,0_2px_8px_rgba(14,165,233,0.35),inset_0_1px_0_rgba(255,255,255,0.3)]"
    ),
    // hover (not applied when active)
    !isActive && (isDark ? "hover:from-[#48484d] hover:to-[#363638]" : "hover:from-[#fafafa] hover:to-[#e8e8eb]"),
    keyClassName,
  );

  const pressVariants: Variants = reduce
    ? {}
    : {
        initial: { y: 0, scale: 1 },
        press:   { y: 1,  scale: 0.97 },
      };

  const handleClick = React.useCallback(() => onPress(keyDef), [onPress, keyDef]);

  return (
    <motion.button
      type="button"
      aria-label={keyDef.label === "⌫" ? "Backspace" : keyDef.label || keyDef.id}
      aria-pressed={isActive}
      style={{ flex: keyDef.width }}
      animate={isActive && !reduce ? "press" : "initial"}
      variants={pressVariants}
      transition={{ type: "spring", stiffness: 500, damping: 30, mass: 0.6 }}
      whileTap={reduce ? undefined : "press"}
      className={baseKey}
      onClick={handleClick}
    >
      {isSpace ? null : (
        <span className="relative flex flex-col items-center justify-end gap-0">
          {/* shift-layer label */}
          {keyDef.sub && !isModifier && !isFnRow && (
            <span
              className={cn(
                "absolute -top-2 left-0 text-[8px] leading-none",
                isDark ? "text-neutral-400" : "text-neutral-400",
                isActive && "text-sky-100",
              )}
            >
              {keyDef.sub}
            </span>
          )}
          <span>{keyDef.label}</span>
        </span>
      )}
    </motion.button>
  );
});
next-ui/keyboard/keyboard-layout.ts
export type KeyKind = "normal" | "modifier" | "space" | "enter" | "delete" | "fn" | "nav";
export type SoundVariant = "normal" | "space" | "enter" | "delete" | "modifier";

export interface KeyDef {
  id: string;
  label: string;
  sub?: string;       // shift-layer label shown smaller
  code: string;       // KeyboardEvent.code
  char: string;       // character to insert
  width: number;      // flex units
  kind: KeyKind;
  sound: SoundVariant;
  groupGap?: boolean; // add extra gap before this key (fn row groups)
}

type Row = KeyDef[];

const k = (
  id: string,
  label: string,
  sub: string | undefined,
  code: string,
  char: string,
  width: number,
  kind: KeyKind = "normal",
  sound: SoundVariant = "normal",
  groupGap = false,
): KeyDef => ({ id, label, sub, code, char, width, kind, sound, groupGap });

// Every row except fn sums to 15 flex-units so rows stay aligned.
export const MAC_ROWS: Row[] = [
  // ── Function row ─────────────────────────────────────────────
  // Esc=1.5u + 12 F-keys=1u each + 3 inter-group gaps → fills naturally via flex
  [
    k("Escape", "esc", undefined, "Escape", "", 1.5, "modifier", "modifier"),
    k("F1",  "F1",  undefined, "F1",  "", 1, "fn", "modifier", true),
    k("F2",  "F2",  undefined, "F2",  "", 1, "fn", "modifier"),
    k("F3",  "F3",  undefined, "F3",  "", 1, "fn", "modifier"),
    k("F4",  "F4",  undefined, "F4",  "", 1, "fn", "modifier"),
    k("F5",  "F5",  undefined, "F5",  "", 1, "fn", "modifier", true),
    k("F6",  "F6",  undefined, "F6",  "", 1, "fn", "modifier"),
    k("F7",  "F7",  undefined, "F7",  "", 1, "fn", "modifier"),
    k("F8",  "F8",  undefined, "F8",  "", 1, "fn", "modifier"),
    k("F9",  "F9",  undefined, "F9",  "", 1, "fn", "modifier", true),
    k("F10", "F10", undefined, "F10", "", 1, "fn", "modifier"),
    k("F11", "F11", undefined, "F11", "", 1, "fn", "modifier"),
    k("F12", "F12", undefined, "F12", "", 1, "fn", "modifier"),
  ],

  // ── Number row (15u) ─────────────────────────────────────────
  [
    k("Backquote",   "`",  "~",  "Backquote",   "`",  1),
    k("Digit1",      "1",  "!",  "Digit1",      "1",  1),
    k("Digit2",      "2",  "@",  "Digit2",      "2",  1),
    k("Digit3",      "3",  "#",  "Digit3",      "3",  1),
    k("Digit4",      "4",  "$",  "Digit4",      "4",  1),
    k("Digit5",      "5",  "%",  "Digit5",      "5",  1),
    k("Digit6",      "6",  "^",  "Digit6",      "6",  1),
    k("Digit7",      "7",  "&",  "Digit7",      "7",  1),
    k("Digit8",      "8",  "*",  "Digit8",      "8",  1),
    k("Digit9",      "9",  "(",  "Digit9",      "9",  1),
    k("Digit0",      "0",  ")",  "Digit0",      "0",  1),
    k("Minus",       "-",  "_",  "Minus",       "-",  1),
    k("Equal",       "=",  "+",  "Equal",       "=",  1),
    k("Backspace",   "⌫",  undefined, "Backspace", "", 2, "delete", "delete"),
  ],

  // ── QWERTY row (15u) ─────────────────────────────────────────
  [
    k("Tab",          "⇥",  undefined, "Tab",          "\t", 1.5, "modifier", "modifier"),
    k("KeyQ",         "Q",  undefined, "KeyQ",         "q",  1),
    k("KeyW",         "W",  undefined, "KeyW",         "w",  1),
    k("KeyE",         "E",  undefined, "KeyE",         "e",  1),
    k("KeyR",         "R",  undefined, "KeyR",         "r",  1),
    k("KeyT",         "T",  undefined, "KeyT",         "t",  1),
    k("KeyY",         "Y",  undefined, "KeyY",         "y",  1),
    k("KeyU",         "U",  undefined, "KeyU",         "u",  1),
    k("KeyI",         "I",  undefined, "KeyI",         "i",  1),
    k("KeyO",         "O",  undefined, "KeyO",         "o",  1),
    k("KeyP",         "P",  undefined, "KeyP",         "p",  1),
    k("BracketLeft",  "[",  "{",  "BracketLeft",  "[",  1),
    k("BracketRight", "]",  "}",  "BracketRight", "]",  1),
    k("Backslash",    "\\", "|",  "Backslash",    "\\", 1.5, "modifier", "modifier"),
  ],

  // ── ASDF row (15u) ───────────────────────────────────────────
  [
    k("CapsLock", "caps",   undefined, "CapsLock", "",   1.75, "modifier", "modifier"),
    k("KeyA",     "A",      undefined, "KeyA",     "a",  1),
    k("KeyS",     "S",      undefined, "KeyS",     "s",  1),
    k("KeyD",     "D",      undefined, "KeyD",     "d",  1),
    k("KeyF",     "F",      undefined, "KeyF",     "f",  1),
    k("KeyG",     "G",      undefined, "KeyG",     "g",  1),
    k("KeyH",     "H",      undefined, "KeyH",     "h",  1),
    k("KeyJ",     "J",      undefined, "KeyJ",     "j",  1),
    k("KeyK",     "K",      undefined, "KeyK",     "k",  1),
    k("KeyL",     "L",      undefined, "KeyL",     "l",  1),
    k("Semicolon",";" ,     ":",  "Semicolon",";" , 1),
    k("Quote",    "'",      "\"", "Quote",    "'",  1),
    k("Enter",    "return", undefined, "Enter",    "\n", 2.25, "enter", "enter"),
  ],

  // ── ZXCV row (15u) ───────────────────────────────────────────
  [
    k("ShiftLeft",  "⇧", undefined, "ShiftLeft",  "",  2.25, "modifier", "modifier"),
    k("KeyZ",       "Z", undefined, "KeyZ",       "z", 1),
    k("KeyX",       "X", undefined, "KeyX",       "x", 1),
    k("KeyC",       "C", undefined, "KeyC",       "c", 1),
    k("KeyV",       "V", undefined, "KeyV",       "v", 1),
    k("KeyB",       "B", undefined, "KeyB",       "b", 1),
    k("KeyN",       "N", undefined, "KeyN",       "n", 1),
    k("KeyM",       "M", undefined, "KeyM",       "m", 1),
    k("Comma",      ",", "<",  "Comma",      ",", 1),
    k("Period",     ".", ">",  "Period",     ".", 1),
    k("Slash",      "/", "?",  "Slash",      "/", 1),
    k("ShiftRight", "⇧", undefined, "ShiftRight", "",  2.75, "modifier", "modifier"),
  ],

  // ── Bottom row (15u) ─────────────────────────────────────────
  [
    k("Fn",          "fn",      undefined, "Fn",          "", 1,    "modifier", "modifier"),
    k("ControlLeft", "control", undefined, "ControlLeft", "", 1.5,  "modifier", "modifier"),
    k("AltLeft",     "option",  undefined, "AltLeft",     "", 1.5,  "modifier", "modifier"),
    k("MetaLeft",    "⌘",       undefined, "MetaLeft",    "", 1.75, "modifier", "modifier"),
    k("Space",       "",        undefined, "Space",       " ", 5,   "space",    "space"),
    k("MetaRight",   "⌘",       undefined, "MetaRight",   "", 1.75, "modifier", "modifier"),
    k("AltRight",    "option",  undefined, "AltRight",    "", 1.5,  "modifier", "modifier"),
    k("ArrowLeft",   "←",       undefined, "ArrowLeft",   "", 1,    "nav",      "modifier"),
    k("ArrowRight",  "→",       undefined, "ArrowRight",  "", 1,    "nav",      "modifier"),
  ],
];

export const COMPACT_ROWS: Row[] = MAC_ROWS.slice(1);

// Maps alternate code names browsers emit for the same physical key
export const CODE_ALIASES: Record<string, string> = {
  OSLeft:  "MetaLeft",
  OSRight: "MetaRight",
};
next-ui/keyboard/keyboard.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import { KeyboardKey } from "./keyboard-key";
import { useKeyboardSound, type SoundType } from "./use-keyboard-sound";
import { MAC_ROWS, COMPACT_ROWS, CODE_ALIASES, type KeyDef } from "./keyboard-layout";

export interface KeyboardProps {
  layout?: "mac" | "compact";
  enableSound?: boolean;
  volume?: number;
  soundType?: SoundType;
  /** Keys currently held/highlighted — pass KeyboardEvent.code values */
  activeKeys?: string[];
  className?: string;
  keyClassName?: string;
  theme?: "dark" | "light" | "auto";
  showFunctionRow?: boolean;
  onKeyPress?: (key: KeyDef) => void;
}

export function MacKeyboard({
  layout = "mac",
  enableSound = true,
  volume = 0.6,
  soundType = "mechanical",
  activeKeys = [],
  className,
  keyClassName,
  theme = "auto",
  showFunctionRow = true,
  onKeyPress,
}: KeyboardProps) {
  const [resolvedTheme, setResolvedTheme] = React.useState<"dark" | "light">("dark");
  const activeSet = React.useMemo(() => new Set(activeKeys), [activeKeys]);
  const { play } = useKeyboardSound({ enabled: enableSound, volume, soundType });

  // Resolve "auto" theme against the document class
  React.useEffect(() => {
    if (theme !== "auto") { setResolvedTheme(theme); return; }
    const update = () =>
      setResolvedTheme(document.documentElement.classList.contains("dark") ? "dark" : "light");
    update();
    const mo = new MutationObserver(update);
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
    return () => mo.disconnect();
  }, [theme]);

  const isDark = resolvedTheme === "dark";

  const rows = React.useMemo(() => {
    const base = layout === "compact" ? COMPACT_ROWS : MAC_ROWS;
    return showFunctionRow ? base : base.slice(layout === "mac" ? 1 : 0);
  }, [layout, showFunctionRow]);

  const handlePress = React.useCallback(
    (key: KeyDef) => {
      play(key.sound);
      onKeyPress?.(key);
    },
    [play, onKeyPress],
  );

  return (
    <div
      role="group"
      aria-label="Mac keyboard"
      className={cn(
        // body shape + material
        "w-full max-w-[780px] mx-auto select-none",
        "rounded-[14px] p-3",
        isDark
          ? "bg-gradient-to-b from-[#242428] to-[#1a1a1e] shadow-[0_12px_40px_rgba(0,0,0,0.6),0_4px_12px_rgba(0,0,0,0.4),inset_0_1px_0_rgba(255,255,255,0.06)]"
          : "bg-gradient-to-b from-[#d8d8dc] to-[#c8c8cc] shadow-[0_12px_40px_rgba(0,0,0,0.18),0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.7)]",
        className,
      )}
    >
      <div className="flex flex-col gap-[5px]">
        {rows.map((row, rowIdx) => {
          const isFnRow = layout === "mac" && showFunctionRow && rowIdx === 0;
          return (
            <div
              key={rowIdx}
              className="flex gap-[4px]"
            >
              {row.map((keyDef) => (
                <React.Fragment key={keyDef.id}>
                  {/* Extra gap for function-key groups */}
                  {keyDef.groupGap && (
                    <div className={isFnRow ? "w-[10px] shrink-0" : undefined} />
                  )}
                  <KeyboardKey
                    keyDef={keyDef}
                    isActive={
                      activeSet.has(keyDef.code) ||
                      (keyDef.code === "MetaLeft"  && activeSet.has("OSLeft")) ||
                      (keyDef.code === "MetaRight" && activeSet.has("OSRight"))
                    }
                    isFnRow={isFnRow}
                    theme={resolvedTheme}
                    keyClassName={keyClassName}
                    onPress={handlePress}
                  />
                </React.Fragment>
              ))}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ── Hook: sync physical keyboard presses to activeKeys ──────────
export function usePhysicalKeyboard() {
  const [pressed, setPressed] = React.useState<Set<string>>(new Set());

  React.useEffect(() => {
    function onDown(e: KeyboardEvent) {
      const code = CODE_ALIASES[e.code] ?? e.code;
      setPressed((p) => { const n = new Set(p); n.add(code); return n; });
    }
    function onUp(e: KeyboardEvent) {
      const code = CODE_ALIASES[e.code] ?? e.code;
      setPressed((p) => { const n = new Set(p); n.delete(code); return n; });
    }
    window.addEventListener("keydown", onDown);
    window.addEventListener("keyup", onUp);
    return () => {
      window.removeEventListener("keydown", onDown);
      window.removeEventListener("keyup", onUp);
    };
  }, []);

  return Array.from(pressed);
}
next-ui/keyboard/use-keyboard-sound.ts
"use client";

import * as React from "react";
import type { SoundVariant } from "./keyboard-layout";

export type SoundType = "mechanical" | "soft" | "clicky";

// Per-variant parameters tuned for each key type
type VParams = {
  // Click layer: the switch mechanism actuating (~first few ms)
  ckHz:  number; ckQ:  number; ckDcy: number; ckGn: number;
  // Thud layer: keycap bottom-out impact (~slightly delayed)
  tdHz:  number; tdQ:  number; tdDcy: number; tdGn: number;
  tdOff: number; // seconds after the click
  // Body-resonance tone
  bodyHz: number; bodyGn: number;
};

const VARIANT: Record<SoundVariant, VParams> = {
  normal:   { ckHz:5400, ckQ:4.2, ckDcy:0.0042, ckGn:0.50, tdHz:400, tdQ:1.1, tdDcy:0.022, tdGn:0.82, tdOff:0.007, bodyHz:280, bodyGn:0.22 },
  space:    { ckHz:2200, ckQ:2.2, ckDcy:0.010,  ckGn:0.28, tdHz:148, tdQ:0.8, tdDcy:0.048, tdGn:1.05, tdOff:0.012, bodyHz:118, bodyGn:0.30 },
  enter:    { ckHz:3700, ckQ:3.4, ckDcy:0.006,  ckGn:0.43, tdHz:275, tdQ:1.0, tdDcy:0.032, tdGn:0.92, tdOff:0.009, bodyHz:205, bodyGn:0.26 },
  delete:   { ckHz:6100, ckQ:4.8, ckDcy:0.0034, ckGn:0.55, tdHz:510, tdQ:1.3, tdDcy:0.017, tdGn:0.73, tdOff:0.005, bodyHz:345, bodyGn:0.18 },
  modifier: { ckHz:3000, ckQ:2.7, ckDcy:0.005,  ckGn:0.28, tdHz:210, tdQ:0.9, tdDcy:0.019, tdGn:0.60, tdOff:0.007, bodyHz:158, bodyGn:0.15 },
};

// Per-type multipliers
const TYPE_MULT: Record<SoundType, { ckScale: number; tdScale: number; bodyScale: number; ckQMult: number }> = {
  mechanical: { ckScale: 1.00, tdScale: 1.00, bodyScale: 1.00, ckQMult: 1.0 },
  soft:       { ckScale: 0.28, tdScale: 0.75, bodyScale: 0.60, ckQMult: 0.5 },
  clicky:     { ckScale: 1.55, tdScale: 0.85, bodyScale: 0.55, ckQMult: 1.9 },
};

function synthesise(ctx: AudioContext, variant: SoundVariant, type: SoundType, volume: number) {
  const now = ctx.currentTime;
  const sr  = ctx.sampleRate;
  const vol = Math.max(0, Math.min(1, volume));
  const p   = VARIANT[variant];
  const tm  = TYPE_MULT[type];

  // Slight per-hit randomisation — keeps rapid typing feeling organic
  const jitter = () => 0.92 + Math.random() * 0.16;

  function noiseBurst(t: number, hz: number, q: number, decaySec: number, gain: number) {
    if (gain < 0.001) return;
    const dur = Math.max(decaySec * 6.5, 0.014);
    const len = Math.ceil(sr * dur);
    const buf = ctx.createBuffer(1, len, sr);
    const d   = buf.getChannelData(0);
    const tc  = decaySec * sr;
    for (let i = 0; i < len; i++)
      d[i] = (Math.random() * 2 - 1) * Math.exp(-i / tc);

    const src = ctx.createBufferSource(); src.buffer = buf;
    const bp  = ctx.createBiquadFilter();
    bp.type = "bandpass"; bp.frequency.value = hz * jitter(); bp.Q.value = q;
    const gn  = ctx.createGain();
    gn.gain.setValueAtTime(gain * vol, t);
    gn.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    src.connect(bp); bp.connect(gn); gn.connect(ctx.destination);
    src.start(t); src.stop(t + dur + 0.01);
  }

  // ── Layer 1: High-frequency click (switch actuation snap) ─────
  noiseBurst(now, p.ckHz, p.ckQ * tm.ckQMult, p.ckDcy, p.ckGn * tm.ckScale);

  // ── Layer 2: Low-frequency thud (bottom-out impact) ───────────
  const t2 = now + p.tdOff;
  noiseBurst(t2, p.tdHz * jitter(), p.tdQ, p.tdDcy, p.tdGn * tm.tdScale);

  // ── Layer 3: Pitched resonance (case / plate ring) ────────────
  const bodyGain = p.bodyGn * tm.bodyScale * vol;
  if (bodyGain > 0.01) {
    const osc = ctx.createOscillator();
    const gn  = ctx.createGain();
    osc.type = "sine";
    osc.frequency.setValueAtTime(p.bodyHz * 1.35 * jitter(), t2);
    osc.frequency.exponentialRampToValueAtTime(p.bodyHz * 0.52, t2 + 0.065);
    gn.gain.setValueAtTime(bodyGain, t2);
    gn.gain.exponentialRampToValueAtTime(0.0001, t2 + 0.085);
    osc.connect(gn); gn.connect(ctx.destination);
    osc.start(t2); osc.stop(t2 + 0.1);
  }
}

export function useKeyboardSound(opts: {
  enabled: boolean;
  volume: number;
  soundType: SoundType;
}) {
  const ctxRef    = React.useRef<AudioContext | null>(null);
  const optsRef   = React.useRef(opts);
  optsRef.current = opts;

  const getCtx = React.useCallback((): AudioContext | null => {
    if (!optsRef.current.enabled) return null;
    try {
      if (!ctxRef.current || ctxRef.current.state === "closed")
        ctxRef.current = new AudioContext();
      if (ctxRef.current.state === "suspended")
        void ctxRef.current.resume();
      return ctxRef.current;
    } catch {
      return null;
    }
  }, []);

  const play = React.useCallback(
    (variant: SoundVariant) => {
      const ctx = getCtx();
      if (!ctx) return;
      try {
        synthesise(ctx, variant, optsRef.current.soundType, optsRef.current.volume);
      } catch {
        // Silently absorb Web Audio errors (policy / SSR)
      }
    },
    [getCtx],
  );

  React.useEffect(() => () => { ctxRef.current?.close().catch(() => {}); }, []);

  return { play };
}
next-ui/mac-keyboard.tsx
export { MacKeyboard, usePhysicalKeyboard } from "./keyboard";
export type { KeyboardProps } from "./keyboard";
export type { KeyDef, KeyKind, SoundVariant } from "./keyboard/keyboard-layout";

More from nexus-labs

Browse all components in this library →