# Mechanical Keyboard (`mechanical-keyboard`)

> Full Mac keyboard with tactile depth-button keycaps — lifts on hover, snaps on press, plays synthesised mechanical click sounds.

- **Docs:** https://www.mellowui.com/components/mechanical-keyboard
- **Markdown:** https://www.mellowui.com/components/mechanical-keyboard.md
- **Registry:** https://www.mellowui.com/r/mechanical-keyboard.json
- **Tool prompt:** https://www.mellowui.com/api/prompt/mechanical-keyboard
- **Categories:** interactive, 3d, audio
- **Dependencies:** none

## AI prompt

Add a MechanicalKeyboard component from the mellow library. It renders a full Mac keyboard where every key has a raised 3D face that lifts on hover and snaps down on press, with synthesised mechanical click sounds. Props: enableSound (bool, default true), showPreview (bool — floating keystroke labels above the board), variant ('default'|'rgb' — rgb is a backlit board with per-key rainbow underglow where each press fires a color ripple across neighbouring keys; theme-adaptive, dark caps in dark mode and white caps in light mode), keyDepth (px, default 3), onKeyPress (code: string) => void. Wrap in overflow-x:auto for mobile.

## Install

```bash
npx shadcn@latest add https://mellowui.com/r/mechanical-keyboard.json
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `enableSound` | `boolean` | `true` | Play WebAudio mechanical click sounds on press and release. |
| `showPreview` | `boolean` | `false` | Float key-legend ghosts above the board as keys are pressed. |
| `variant` | `"default" \| "rgb"` | `"default"` | default = themed ink glass. rgb = backlit board with per-key rainbow underglow — each press ripples color across neighbouring keys. Adapts to theme: dark caps in dark mode, white caps in light mode. |
| `keyDepth` | `number` | `3` | Height of the raised keycap block in pixels. |
| `onKeyPress` | `(code: string) => void` | — | Callback fired on each key press with the KeyboardEvent.code value. |
| `className` | `string` | — | Additional CSS classes for the outer wrapper. |

## Design notes

- Client component (`"use client"`) — required for animation and refs.
- Uses Mellow design tokens: `--ink`, `--ink-rgb`, `--background`, `--background-rgb`, `--rule`, `--font-sans`, `--font-serif`, `--font-mono`. Tokens auto-flip between light and dark themes — never hardcode colors.
- Respects `prefers-reduced-motion: reduce` where applicable.
- Self-contained — no shared utilities. Copy-paste, not a package.

## Source

```tsx
"use client";

import React, { useState, useRef, useEffect, useCallback } from "react";

type KeySoundType = "alpha" | "modifier" | "wide" | "fn";
type SoundPhase = "press" | "release";

type SoundRow = [number, number, number, number, number];

const SOUND_PARAMS: Record<KeySoundType, [SoundRow, SoundRow]> = {
  alpha:    [[0.040, 7,  0.72, 2800, 0.56], [0.026, 11, 0.48, 5500, 0.40]],
  modifier: [[0.035, 8,  0.52, 2000, 0.44], [0.024, 12, 0.36, 4000, 0.30]],
  wide:     [[0.060, 5,  0.85, 1000, 0.65], [0.040,  8, 0.60, 2000, 0.45]],
  fn:       [[0.032, 8,  0.60, 3500, 0.48], [0.022, 12, 0.42, 6000, 0.32]],
};

function playKeySound(ctx: AudioContext, phase: SoundPhase, type: KeySoundType) {
  const [dur, decay, amp, lpHz, gain] = SOUND_PARAMS[type][phase === "press" ? 0 : 1];
  const len = Math.floor(ctx.sampleRate * dur);
  const buf = ctx.createBuffer(1, len, ctx.sampleRate);
  const data = buf.getChannelData(0);
  for (let i = 0; i < len; i++) {
    data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, decay) * amp;
  }
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const lp = ctx.createBiquadFilter();
  lp.type = "lowpass";
  lp.frequency.value = lpHz;
  const g = ctx.createGain();
  g.gain.value = gain;
  src.connect(lp);
  lp.connect(g);
  g.connect(ctx.destination);
  src.start();
}

interface KeyDef {
  code: string;
  label: string;
  sub?: string;
  w?: number;
  sound?: KeySoundType;
}

const KB_W = 680;
const KEY_H = 42;
const FN_H = 30;
const GAP = 4;
const ARROW_H = Math.floor((KEY_H - GAP) / 2);

const FN_ROW: KeyDef[] = [
  { code: "Escape", label: "esc",  w: 1.5, sound: "modifier" },
  { code: "F1",  label: "F1",  sound: "fn" },
  { code: "F2",  label: "F2",  sound: "fn" },
  { code: "F3",  label: "F3",  sound: "fn" },
  { code: "F4",  label: "F4",  sound: "fn" },
  { code: "F5",  label: "F5",  sound: "fn" },
  { code: "F6",  label: "F6",  sound: "fn" },
  { code: "F7",  label: "F7",  sound: "fn" },
  { code: "F8",  label: "F8",  sound: "fn" },
  { code: "F9",  label: "F9",  sound: "fn" },
  { code: "F10", label: "F10", sound: "fn" },
  { code: "F11", label: "F11", sound: "fn" },
  { code: "F12", label: "F12", sound: "fn" },
];

const MAIN_ROWS: KeyDef[][] = [
  [
    { code: "Backquote",    label: "`",  sub: "~"  },
    { code: "Digit1",       label: "1",  sub: "!"  },
    { code: "Digit2",       label: "2",  sub: "@"  },
    { code: "Digit3",       label: "3",  sub: "#"  },
    { code: "Digit4",       label: "4",  sub: "$"  },
    { code: "Digit5",       label: "5",  sub: "%"  },
    { code: "Digit6",       label: "6",  sub: "^"  },
    { code: "Digit7",       label: "7",  sub: "&"  },
    { code: "Digit8",       label: "8",  sub: "*"  },
    { code: "Digit9",       label: "9",  sub: "("  },
    { code: "Digit0",       label: "0",  sub: ")"  },
    { code: "Minus",        label: "-",  sub: "_"  },
    { code: "Equal",        label: "=",  sub: "+"  },
    { code: "Backspace",    label: "delete", w: 2,    sound: "wide" },
  ],
  [
    { code: "Tab",          label: "tab",  w: 1.5, sound: "modifier" },
    { code: "KeyQ", label: "Q" },
    { code: "KeyW", label: "W" },
    { code: "KeyE", label: "E" },
    { code: "KeyR", label: "R" },
    { code: "KeyT", label: "T" },
    { code: "KeyY", label: "Y" },
    { code: "KeyU", label: "U" },
    { code: "KeyI", label: "I" },
    { code: "KeyO", label: "O" },
    { code: "KeyP", label: "P" },
    { code: "BracketLeft",  label: "[",  sub: "{"  },
    { code: "BracketRight", label: "]",  sub: "}"  },
    { code: "Backslash",    label: "\\", sub: "|",  w: 1.5 },
  ],
  [
    { code: "CapsLock",     label: "caps",   w: 1.75, sound: "modifier" },
    { code: "KeyA", label: "A" },
    { code: "KeyS", label: "S" },
    { code: "KeyD", label: "D" },
    { code: "KeyF", label: "F" },
    { code: "KeyG", label: "G" },
    { code: "KeyH", label: "H" },
    { code: "KeyJ", label: "J" },
    { code: "KeyK", label: "K" },
    { code: "KeyL", label: "L" },
    { code: "Semicolon",    label: ";",  sub: ":"  },
    { code: "Quote",        label: "'",  sub: '"'  },
    { code: "Enter",        label: "return", w: 2.25, sound: "wide" },
  ],
  [
    { code: "ShiftLeft",    label: "shift", w: 2.25, sound: "modifier" },
    { code: "KeyZ", label: "Z" },
    { code: "KeyX", label: "X" },
    { code: "KeyC", label: "C" },
    { code: "KeyV", label: "V" },
    { code: "KeyB", label: "B" },
    { code: "KeyN", label: "N" },
    { code: "KeyM", label: "M" },
    { code: "Comma",        label: ",",  sub: "<"  },
    { code: "Period",       label: ".",  sub: ">"  },
    { code: "Slash",        label: "/",  sub: "?"  },
    { code: "ShiftRight",   label: "shift", w: 2.75, sound: "modifier" },
  ],
];

const BOTTOM_ROW: KeyDef[] = [
  { code: "Fn",          label: "fn",   w: 1.25, sound: "modifier" },
  { code: "ControlLeft", label: "ctrl", w: 1.25, sound: "modifier" },
  { code: "AltLeft",     label: "opt",  w: 1.25, sound: "modifier" },
  { code: "MetaLeft",    label: "cmd",  w: 1.5,  sound: "modifier" },
  { code: "Space",       label: "",     w: 6.25, sound: "wide"     },
  { code: "MetaRight",   label: "cmd",  w: 1.5,  sound: "modifier" },
  { code: "AltRight",    label: "opt",  w: 1.25, sound: "modifier" },
];

const ARROW_UP_DEF: KeyDef   = { code: "ArrowUp",    label: "↑", sound: "modifier" };
const ARROW_ROW_DEFS: KeyDef[] = [
  { code: "ArrowLeft",  label: "←", sound: "modifier" },
  { code: "ArrowDown",  label: "↓", sound: "modifier" },
  { code: "ArrowRight", label: "→", sound: "modifier" },
];

const ALL_DEFS: KeyDef[] = [
  ...FN_ROW,
  ...MAIN_ROWS.flat(),
  ...BOTTOM_ROW,
  ARROW_UP_DEF,
  ...ARROW_ROW_DEFS,
];

const ALL_CODES = new Set(ALL_DEFS.map(d => d.code));

// Per-key backlight hue for the rgb variant — a rainbow swept left→right
// across each row, drifting +12° per row for a diagonal wave.
const KEY_HUE: Record<string, number> = (() => {
  const hues: Record<string, number> = {};
  const assign = (row: KeyDef[], rowIdx: number, extraUnits = 0) => {
    const total = row.reduce((s, d) => s + (d.w ?? 1), 0) + extraUnits;
    let acc = 0;
    for (const d of row) {
      const center = acc + (d.w ?? 1) / 2;
      acc += d.w ?? 1;
      hues[d.code] = ((center / total) * 300 + rowIdx * 12) % 360;
    }
    return { acc, total };
  };
  assign(FN_ROW, 0);
  MAIN_ROWS.forEach((r, i) => assign(r, i + 1));
  // Bottom row shares its flex track with the arrow cluster (flex 3).
  const { acc, total } = assign(BOTTOM_ROW, 5, 3);
  hues[ARROW_UP_DEF.code] = (((acc + 1.5) / total) * 300 + 5 * 12) % 360;
  ARROW_ROW_DEFS.forEach((d, i) => {
    hues[d.code] = (((acc + i + 0.5) / total) * 300 + 5 * 12) % 360;
  });
  return hues;
})();

// Theme detection for the rgb variant — bright ink means dark theme. Reads
// the design-token var (CSS vars can't be used inside the per-hue oklch
// palettes below) and re-reads when the theme class flips on <html>.
function useThemeIsDark(): boolean {
  const [dark, setDark] = useState(true);
  useEffect(() => {
    const read = () => {
      const raw = getComputedStyle(document.documentElement)
        .getPropertyValue("--ink-rgb");
      const [r, g, b] = raw.split(",").map(n => parseFloat(n));
      if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {
        setDark((r + g + b) / 3 > 127);
      }
    };
    read();
    const mo = new MutationObserver(read);
    mo.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class", "data-theme"],
    });
    return () => mo.disconnect();
  }, []);
  return dark;
}

interface SingleKeyProps {
  def: KeyDef;
  active: boolean;
  depth: number;
  rgb: boolean;
  dark: boolean;
  height: number;
  onPress: () => void;
  onRelease: () => void;
  glowRef: (el: HTMLSpanElement | null) => void;
}

function SingleKey({
  def,
  active,
  depth,
  rgb,
  dark,
  height,
  onPress,
  onRelease,
  glowRef,
}: SingleKeyProps) {
  const [hovered, setHovered]   = useState(false);
  const [mouseDown, setMouseDown] = useState(false);
  const pressed = active || mouseDown;

  const isModLike = def.sound === "modifier" || def.sound === "fn";
  const hasSub    = Boolean(def.sub);
  const isWide    = (def.w ?? 1) > 1.2;

  const labelSize = isModLike ? "8px" : hasSub ? "10px" : "11px";
  const hue = KEY_HUE[def.code] ?? 250;

  return (
    <div
      style={{
        flex: `${def.w ?? 1} 1 0`,
        minWidth: 0,
        position: "relative",
        height,
        cursor: "pointer",
        userSelect: "none",
      }}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => { setHovered(false); setMouseDown(false); }}
      onMouseDown={e => { e.preventDefault(); setMouseDown(true); onPress(); }}
      onMouseUp={() => { setMouseDown(false); onRelease(); }}
    >
      <span
        aria-hidden
        style={{
          position: "absolute",
          inset: 0,
          borderRadius: 5,
          background: rgb
            ? dark
              ? `oklch(0.34 0.17 ${hue})`
              : `oklch(0.78 0.13 ${hue})`
            : "rgba(var(--ink-rgb),0.03)",
          border: `1px solid ${
            rgb
              ? dark
                ? `oklch(0.5 0.2 ${hue} / 0.5)`
                : `oklch(0.65 0.18 ${hue} / 0.6)`
              : "rgba(var(--ink-rgb),0.06)"
          }`,
          boxShadow: rgb
            ? dark
              ? `0 ${depth + 5}px 14px -2px oklch(0.62 0.25 ${hue} / 0.55)`
              : `0 ${depth + 5}px 14px -2px oklch(0.55 0.22 ${hue} / 0.5)`
            : undefined,
          transform: `translateY(${depth}px)`,
        }}
      />

      <span
        style={{
          position: "absolute",
          inset: 0,
          borderRadius: 5,
          display: "flex",
          flexDirection: "column",
          alignItems: hasSub || isWide ? "flex-start" : "center",
          justifyContent: hasSub ? "flex-end" : "center",
          padding: hasSub
            ? "2px 5px 3px"
            : isWide
            ? "0 7px"
            : undefined,
          overflow: "hidden",
          fontFamily: "var(--font-mono)",
          fontSize: labelSize,
          fontWeight: 500,
          letterSpacing: "0.01em",
          color: rgb
            ? dark
              ? `oklch(0.88 0.1 ${hue})`
              : `oklch(0.45 0.18 ${hue})`
            : "var(--ink)",
          whiteSpace: "nowrap",
          background: rgb
            ? dark
              ? "oklch(0.23 0.015 250)"
              : "oklch(0.97 0.005 250)"
            : "rgba(var(--ink-rgb),0.07)",
          border: `1px solid ${
            rgb
              ? dark
                ? `oklch(0.45 0.12 ${hue} / ${pressed ? 0.9 : 0.55})`
                : `oklch(0.6 0.16 ${hue} / ${pressed ? 0.9 : 0.5})`
              : pressed
              ? "rgba(var(--ink-rgb),0.18)"
              : "rgba(var(--ink-rgb),0.12)"
          }`,
          transform: pressed
            ? `translateY(${depth}px)`
            : hovered
            ? "translateY(-1px)"
            : "translateY(0px)",
          transition: pressed
            ? "transform 55ms ease, box-shadow 55ms ease"
            : "transform 300ms cubic-bezier(0.34,1.4,0.64,1), box-shadow 200ms ease",
          boxShadow: pressed
            ? "none"
            : "inset 0 1px 0 rgba(255,255,255,0.08), inset 0 -1px 0 rgba(0,0,0,0.12)",
        }}
      >
        {rgb && (
          <span
            aria-hidden
            ref={glowRef}
            style={{
              position: "absolute",
              inset: 0,
              // screen brightens dark caps; multiply tints white caps —
              // either way the ripple reads as the key's own hue lighting up.
              background: dark
                ? `oklch(0.72 0.23 ${hue})`
                : `oklch(0.82 0.17 ${hue})`,
              mixBlendMode: dark ? "screen" : "multiply",
              opacity: 0,
              pointerEvents: "none",
            }}
          />
        )}
        {hasSub && (
          <span
            style={{
              position: "relative",
              zIndex: 1,
              fontSize: "8px",
              lineHeight: 1,
              color: rgb
                ? dark
                  ? `oklch(0.75 0.06 ${hue} / 0.7)`
                  : `oklch(0.5 0.12 ${hue} / 0.75)`
                : "rgba(var(--ink-rgb),0.38)",
              marginBottom: 1,
            }}
          >
            {def.sub}
          </span>
        )}
        <span style={{ position: "relative", zIndex: 1, lineHeight: 1 }}>{def.label}</span>
      </span>
    </div>
  );
}

export interface MechanicalKeyboardProps {
  className?: string;
  enableSound?: boolean;
  showPreview?: boolean;
  variant?: "default" | "rgb";
  keyDepth?: number;
  onKeyPress?: (code: string) => void;
}

const KB_H_BASE = 12 + FN_H + 5 * GAP + 5 * KEY_H + 16;

// Preview text has no forced line-break — a run typed without ever hitting
// Space is one unbreakable string that CSS can't wrap, so it would otherwise
// overflow the board's width. Cap it well inside the 680px board (mono
// glyphs at 17px run ~10.5px wide) so it always auto-clears before that.
const MAX_PREVIEW_CHARS = 48;

export function MechanicalKeyboard({
  className,
  enableSound = true,
  showPreview = false,
  variant = "default",
  keyDepth = 3,
  onKeyPress,
}: MechanicalKeyboardProps) {
  const [active, setActive] = useState<Set<string>>(new Set());
  const [word, setWord] = useState("");

  const hoveredRef   = useRef(false);
  const audioCtxRef  = useRef<AudioContext | null>(null);
  const pendingRef   = useRef(new Map<string, KeySoundType>());
  const containerRef = useRef<HTMLDivElement>(null);
  const outerRef     = useRef<HTMLDivElement>(null);
  const bodyRef      = useRef<HTMLDivElement>(null);
  const isRgb        = variant === "rgb";
  const isDark       = useThemeIsDark();

  // ── RGB ripple engine ────────────────────────────────────────────
  // On press, an expanding color wavefront lights each key's glow overlay
  // in its own hue. Driven imperatively (direct style.opacity writes) so
  // ~90 keys never re-render per frame.
  const glowEls    = useRef(new Map<string, HTMLSpanElement>());
  const keyCenters = useRef(new Map<string, { x: number; y: number }>());
  const ripplesRef = useRef<{ cx: number; cy: number; t0: number }[]>([]);
  const rippleRaf  = useRef(0);

  const rippleFrame = useCallback((now: number) => {
    const RIPPLE_MS = 650;
    const SPEED = 0.9;   // wavefront px/ms (screen px, scales with board)
    const SIGMA = 34;    // wavefront thickness
    ripplesRef.current = ripplesRef.current.filter(r => now - r.t0 < RIPPLE_MS);
    glowEls.current.forEach((el, code) => {
      const c = keyCenters.current.get(code);
      if (!c) return;
      let g = 0;
      for (const r of ripplesRef.current) {
        const t = now - r.t0;
        const d = Math.hypot(c.x - r.cx, c.y - r.cy);
        const front = t * SPEED;
        g += Math.exp(-((d - front) ** 2) / (2 * SIGMA * SIGMA)) * (1 - t / RIPPLE_MS);
      }
      el.style.opacity = Math.min(1, g).toFixed(3);
    });
    rippleRaf.current = ripplesRef.current.length
      ? requestAnimationFrame(rippleFrame)
      : 0;
  }, []);

  const startRipple = useCallback(
    (code: string) => {
      if (!isRgb) return;
      if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
      // Re-measure at the start of a ripple burst — layout/scale may have
      // changed since the last one, and this forces layout only once.
      if (ripplesRef.current.length === 0) {
        keyCenters.current.clear();
        glowEls.current.forEach((el, c) => {
          const r = el.getBoundingClientRect();
          keyCenters.current.set(c, { x: r.left + r.width / 2, y: r.top + r.height / 2 });
        });
      }
      const center = keyCenters.current.get(code);
      if (!center) return;
      ripplesRef.current.push({ cx: center.x, cy: center.y, t0: performance.now() });
      if (!rippleRaf.current) rippleRaf.current = requestAnimationFrame(rippleFrame);
    },
    [isRgb, rippleFrame],
  );

  useEffect(() => () => cancelAnimationFrame(rippleRaf.current), []);

  const [scale, setScale] = useState(1);
  const [bodyH, setBodyH] = useState(KB_H_BASE);

  useEffect(() => {
    const outer = outerRef.current;
    const body  = bodyRef.current;
    if (!outer || !body) return;
    const ro = new ResizeObserver(() => {
      const availW = outer.getBoundingClientRect().width;
      const bH = body.offsetHeight;
      if (availW > 0) setScale(Math.min(1, availW / KB_W));
      if (bH     > 0) setBodyH(bH);
    });
    ro.observe(outer);
    ro.observe(body);
    return () => ro.disconnect();
  }, []);

  const getCtx = useCallback((): AudioContext | null => {
    if (!enableSound) return null;
    try {
      const AC =
        window.AudioContext ||
        (window as unknown as { webkitAudioContext: typeof AudioContext })
          .webkitAudioContext;
      if (!audioCtxRef.current || audioCtxRef.current.state === "closed") {
        audioCtxRef.current = new AC();
      }
      return audioCtxRef.current;
    } catch {
      return null;
    }
  }, [enableSound]);

  const sound = useCallback(
    (phase: SoundPhase, type: KeySoundType = "alpha") => {
      const ctx = getCtx();
      if (!ctx) return;
      const go = () => playKeySound(ctx, phase, type);
      ctx.state === "suspended" ? ctx.resume().then(go) : go();
    },
    [getCtx],
  );

  const handlePreviewKey = useCallback(
    (def: KeyDef, physicalKey?: string) => {
      if (!showPreview) return;
      if (def.code === "Space") { setWord(""); return; }
      if (def.code === "Backspace") { setWord(w => w.slice(0, -1)); return; }
      if (def.sound === "modifier" || def.sound === "fn") return;
      if (def.code === "Enter") return;
      let ch: string;
      if (physicalKey && physicalKey.length === 1) {
        ch = physicalKey;
      } else if (def.label.length === 1) {
        ch = def.label.toLowerCase();
      } else {
        return;
      }
      setWord(w => (w.length >= MAX_PREVIEW_CHARS ? ch : w + ch));
    },
    [showPreview],
  );

  const pressKey = useCallback(
    (def: KeyDef, physicalKey?: string) => {
      sound("press", def.sound ?? "alpha");
      pendingRef.current.set(def.code, def.sound ?? "alpha");
      startRipple(def.code);
      handlePreviewKey(def, physicalKey);
      onKeyPress?.(def.code);
    },
    [sound, startRipple, handlePreviewKey, onKeyPress],
  );

  const releaseKey = useCallback(
    (def: KeyDef) => {
      const type = pendingRef.current.get(def.code);
      if (type !== undefined) {
        sound("release", type);
        pendingRef.current.delete(def.code);
      }
    },
    [sound],
  );

  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      if (!hoveredRef.current || e.repeat || !ALL_CODES.has(e.code)) return;
      // Space/arrows/Tab scroll or shift focus by default — this keyboard
      // owns the keystroke while hovered, so stop the browser from also
      // acting on it (e.g. Space scrolling the docs page to the bottom).
      if (!e.metaKey && !e.ctrlKey && !e.altKey) e.preventDefault();
      setActive(prev => new Set([...prev, e.code]));
      const def = ALL_DEFS.find(d => d.code === e.code);
      if (def) pressKey(def, e.key);
    };

    const up = (e: KeyboardEvent) => {
      if (!ALL_CODES.has(e.code)) return;
      setActive(prev => {
        const next = new Set(prev);
        next.delete(e.code);
        return next;
      });
      const def = ALL_DEFS.find(d => d.code === e.code);
      if (def) releaseKey(def);
    };

    document.addEventListener("keydown", down);
    document.addEventListener("keyup", up);
    return () => {
      document.removeEventListener("keydown", down);
      document.removeEventListener("keyup", up);
    };
  }, [pressKey, releaseKey]);

  const rowProps = (def: KeyDef) => ({
    def,
    active: active.has(def.code),
    depth: keyDepth,
    rgb: isRgb,
    dark: isDark,
    onPress: () => pressKey(def),
    onRelease: () => releaseKey(def),
    glowRef: (el: HTMLSpanElement | null) => {
      if (el) glowEls.current.set(def.code, el);
      else glowEls.current.delete(def.code);
    },
  });

  return (
    <div
      ref={outerRef}
      className={className}
      style={{
        width: "100%",
        position: "relative",
        overflow: "hidden",
        height: bodyH * scale,
      }}
      onMouseEnter={() => { hoveredRef.current = true; }}
      onMouseLeave={() => { hoveredRef.current = false; }}
    >
      <div
        ref={containerRef}
        style={{
          position: "absolute",
          top: 0,
          left: "50%",
          marginLeft: -(KB_W / 2),
          transformOrigin: "top center",
          transform: `scale(${scale})`,
          width: KB_W,
        }}
      >
      <div
        ref={bodyRef}
        style={{
          display: "inline-flex",
          flexDirection: "column",
          gap: GAP,
          padding: "12px 14px 16px",
          borderRadius: 14,
          background: isRgb
            ? isDark
              ? "oklch(0.16 0.005 250)"
              : "oklch(0.92 0.005 250)"
            : "rgba(var(--ink-rgb),0.04)",
          border: `1px solid ${
            isRgb
              ? isDark
                ? "oklch(0.32 0.03 250 / 0.7)"
                : "oklch(0.78 0.02 250)"
              : "rgba(var(--ink-rgb),0.08)"
          }`,
          width: KB_W,
        }}
      >
        {showPreview && (
          <div
            style={{
              height: 36,
              marginBottom: 4,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              overflow: "hidden",
            }}
          >
            <span
              style={{
                fontFamily: "var(--font-mono)",
                fontSize: "17px",
                fontWeight: 500,
                color: isRgb
                  ? isDark
                    ? "oklch(0.9 0.02 250)"
                    : "oklch(0.25 0.02 250)"
                  : "var(--ink)",
                letterSpacing: "0.08em",
                whiteSpace: "nowrap",
                opacity: word ? 1 : 0,
                transition: "opacity 120ms ease",
                pointerEvents: "none",
                userSelect: "none",
              }}
            >
              {word || " "}
            </span>
          </div>
        )}

        <div style={{ display: "flex", gap: GAP, width: "100%" }}>
          {FN_ROW.map(def => (
            <SingleKey key={def.code} {...rowProps(def)} height={FN_H} />
          ))}
        </div>

        {MAIN_ROWS.map((row, ri) => (
          <div key={ri} style={{ display: "flex", gap: GAP, width: "100%" }}>
            {row.map(def => (
              <SingleKey key={def.code} {...rowProps(def)} height={KEY_H} />
            ))}
          </div>
        ))}

        <div style={{ display: "flex", gap: GAP, width: "100%" }}>
          {BOTTOM_ROW.map(def => (
            <SingleKey key={def.code} {...rowProps(def)} height={KEY_H} />
          ))}

          <div
            style={{
              flex: "3 1 0",
              minWidth: 0,
              display: "flex",
              flexDirection: "column",
              gap: GAP,
            }}
          >
            <div style={{ display: "flex", gap: GAP }}>
              <div style={{ flex: "1 1 0" }} />
              <SingleKey {...rowProps(ARROW_UP_DEF)} height={ARROW_H} />
              <div style={{ flex: "1 1 0" }} />
            </div>
            <div style={{ display: "flex", gap: GAP }}>
              {ARROW_ROW_DEFS.map(def => (
                <SingleKey key={def.code} {...rowProps(def)} height={ARROW_H} />
              ))}
            </div>
          </div>
        </div>
      </div>
      </div>
    </div>
  );
}

export default MechanicalKeyboard;

```

## Demo

```tsx
"use client";

import React, { useState } from "react";
import { MechanicalKeyboard } from "../mellow/mechanical-keyboard";

export default function MechanicalKeyboardDemo() {
  const [sound, setSound]     = useState(true);
  const [preview, setPreview] = useState(false);
  const [rgb, setRgb]         = useState(false);

  return (
    <div className="flex w-full flex-col items-center gap-6 px-3 py-6 sm:px-8">
      {/* Controls */}
      <div className="flex flex-wrap items-center justify-center gap-3">
        {(
          [
            { label: "Sound",   active: sound,   toggle: () => setSound(v => !v) },
            { label: "Preview", active: preview, toggle: () => setPreview(v => !v) },
            { label: "RGB",     active: rgb,     toggle: () => setRgb(v => !v) },
          ] as const
        ).map(({ label, active, toggle }) => (
          <button
            key={label}
            onClick={toggle}
            style={{
              fontFamily: "var(--font-mono)",
              fontSize: "0.6875rem",
              fontWeight: 500,
              letterSpacing: "0.12em",
              textTransform: "uppercase",
              padding: "0.35rem 0.875rem",
              borderRadius: 6,
              border: `1px solid ${
                active
                  ? "rgba(var(--ink-rgb),0.3)"
                  : "rgba(var(--ink-rgb),0.1)"
              }`,
              background: active
                ? "rgba(var(--ink-rgb),0.1)"
                : "transparent",
              color: active ? "var(--ink)" : "rgba(var(--ink-rgb),0.45)",
              cursor: "pointer",
              transition: "all 150ms ease",
            }}
          >
            {active ? "▸ " : ""}{label}
          </button>
        ))}
      </div>

      {/* Keyboard */}
      <MechanicalKeyboard
        enableSound={sound}
        showPreview={preview}
        variant={rgb ? "rgb" : "default"}
      />

      <p
        style={{
          fontFamily: "var(--font-mono)",
          fontSize: "0.6875rem",
          color: "rgba(var(--ink-rgb),0.3)",
          letterSpacing: "0.08em",
          textTransform: "uppercase",
        }}
      >
        hover keyboard · then type
      </p>
    </div>
  );
}

```
