{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "keyboard",
  "title": "Keyboard",
  "description": "A two-tone mechanical keyboard with 3D press animations and physical key highlighting.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/spark-ui/keyboard.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\ninterface KeyDef {\n  /** KeyboardEvent.code used to match physical key presses. */\n  code: string;\n  label: string;\n  /** Shifted symbol rendered above the label (e.g. \"!\" over \"1\"). */\n  sublabel?: string;\n  /** Value passed to onKeyPress; defaults to the lowercased label. */\n  value?: string;\n  /** Relative width in key units (1 = a standard key). */\n  width?: number;\n  /** Dark keycap (two-tone mechanical look). */\n  accent?: boolean;\n  ariaLabel?: string;\n}\n\n// 60% ANSI layout — every row sums to 15 units so keys align across rows.\nconst ROWS: KeyDef[][] = [\n  [\n    { code: \"Backquote\", label: \"`\", sublabel: \"~\" },\n    { code: \"Digit1\", label: \"1\", sublabel: \"!\" },\n    { code: \"Digit2\", label: \"2\", sublabel: \"@\" },\n    { code: \"Digit3\", label: \"3\", sublabel: \"#\" },\n    { code: \"Digit4\", label: \"4\", sublabel: \"$\" },\n    { code: \"Digit5\", label: \"5\", sublabel: \"%\" },\n    { code: \"Digit6\", label: \"6\", sublabel: \"^\" },\n    { code: \"Digit7\", label: \"7\", sublabel: \"&\" },\n    { code: \"Digit8\", label: \"8\", sublabel: \"*\" },\n    { code: \"Digit9\", label: \"9\", sublabel: \"(\" },\n    { code: \"Digit0\", label: \"0\", sublabel: \")\" },\n    { code: \"Minus\", label: \"-\", sublabel: \"_\" },\n    { code: \"Equal\", label: \"=\", sublabel: \"+\" },\n    { code: \"Backspace\", label: \"delete\", value: \"Backspace\", width: 2, accent: true },\n  ],\n  [\n    { code: \"Tab\", label: \"tab\", value: \"Tab\", width: 1.5, accent: true },\n    { code: \"KeyQ\", label: \"Q\" },\n    { code: \"KeyW\", label: \"W\" },\n    { code: \"KeyE\", label: \"E\" },\n    { code: \"KeyR\", label: \"R\" },\n    { code: \"KeyT\", label: \"T\" },\n    { code: \"KeyY\", label: \"Y\" },\n    { code: \"KeyU\", label: \"U\" },\n    { code: \"KeyI\", label: \"I\" },\n    { code: \"KeyO\", label: \"O\" },\n    { code: \"KeyP\", label: \"P\" },\n    { code: \"BracketLeft\", label: \"[\", sublabel: \"{\" },\n    { code: \"BracketRight\", label: \"]\", sublabel: \"}\" },\n    { code: \"Backslash\", label: \"\\\\\", sublabel: \"|\", width: 1.5, accent: true },\n  ],\n  [\n    { code: \"CapsLock\", label: \"caps\", value: \"CapsLock\", width: 1.75, accent: true },\n    { code: \"KeyA\", label: \"A\" },\n    { code: \"KeyS\", label: \"S\" },\n    { code: \"KeyD\", label: \"D\" },\n    { code: \"KeyF\", label: \"F\" },\n    { code: \"KeyG\", label: \"G\" },\n    { code: \"KeyH\", label: \"H\" },\n    { code: \"KeyJ\", label: \"J\" },\n    { code: \"KeyK\", label: \"K\" },\n    { code: \"KeyL\", label: \"L\" },\n    { code: \"Semicolon\", label: \";\", sublabel: \":\" },\n    { code: \"Quote\", label: \"'\", sublabel: '\"' },\n    { code: \"Enter\", label: \"return\", value: \"Enter\", width: 2.25, accent: true },\n  ],\n  [\n    { code: \"ShiftLeft\", label: \"shift\", value: \"Shift\", width: 2.25, accent: true },\n    { code: \"KeyZ\", label: \"Z\" },\n    { code: \"KeyX\", label: \"X\" },\n    { code: \"KeyC\", label: \"C\" },\n    { code: \"KeyV\", label: \"V\" },\n    { code: \"KeyB\", label: \"B\" },\n    { code: \"KeyN\", label: \"N\" },\n    { code: \"KeyM\", label: \"M\" },\n    { code: \"Comma\", label: \",\", sublabel: \"<\" },\n    { code: \"Period\", label: \".\", sublabel: \">\" },\n    { code: \"Slash\", label: \"/\", sublabel: \"?\" },\n    { code: \"ShiftRight\", label: \"shift\", value: \"Shift\", width: 2.75, accent: true },\n  ],\n  [\n    { code: \"ControlLeft\", label: \"control\", value: \"Control\", width: 1.5, accent: true },\n    { code: \"AltLeft\", label: \"option\", value: \"Alt\", width: 1.25, accent: true },\n    { code: \"MetaLeft\", label: \"⌘\", value: \"Meta\", width: 1.5, accent: true, ariaLabel: \"command\" },\n    { code: \"Space\", label: \"\", value: \" \", width: 6.5, ariaLabel: \"space\" },\n    { code: \"MetaRight\", label: \"⌘\", value: \"Meta\", width: 1.5, accent: true, ariaLabel: \"command\" },\n    { code: \"AltRight\", label: \"option\", value: \"Alt\", width: 1.25, accent: true },\n    { code: \"ControlRight\", label: \"control\", value: \"Control\", width: 1.5, accent: true },\n  ],\n];\n\n/**\n * Synthesized ASMR keyboard sound — soft-attack \"pock\", a deep slow thock,\n * and a whisper of close-mic air on press, plus a gentle tick on release,\n * all slightly randomized. No audio files or licenses involved.\n */\nfunction useKeySound(enabled: boolean) {\n  const ctxRef = React.useRef<AudioContext | null>(null);\n  return React.useCallback(\n    (kind: \"down\" | \"up\") => {\n      if (enabled === false || typeof AudioContext === \"undefined\") return;\n      const ctx = (ctxRef.current ??= new AudioContext());\n      if (ctx.state === \"suspended\") void ctx.resume();\n      const t = ctx.currentTime;\n\n      // humanized master chain: slight volume + stereo drift per stroke,\n      // lowpassed so nothing fizzes\n      const out = ctx.createGain();\n      out.gain.value =\n        kind === \"down\" ? 0.85 + Math.random() * 0.15 : 0.3 + Math.random() * 0.1;\n      const smooth = ctx.createBiquadFilter();\n      smooth.type = \"lowpass\";\n      smooth.frequency.value = 5500;\n      const pan = ctx.createStereoPanner();\n      pan.pan.value = (Math.random() - 0.5) * 0.3;\n      out.connect(smooth).connect(pan).connect(ctx.destination);\n\n      const noiseBurst = (\n        freq: number,\n        q: number,\n        gain: number,\n        dur: number,\n      ) => {\n        const buffer = ctx.createBuffer(\n          1,\n          Math.ceil(ctx.sampleRate * dur),\n          ctx.sampleRate,\n        );\n        const data = buffer.getChannelData(0);\n        for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;\n        const src = ctx.createBufferSource();\n        src.buffer = buffer;\n        const bp = ctx.createBiquadFilter();\n        bp.type = \"bandpass\";\n        bp.frequency.value = freq;\n        bp.Q.value = q;\n        const g = ctx.createGain();\n        // soft 5ms attack — no harsh transient, key to the ASMR feel\n        g.gain.setValueAtTime(0.0001, t);\n        g.gain.linearRampToValueAtTime(gain, t + 0.005);\n        g.gain.exponentialRampToValueAtTime(0.001, t + dur);\n        src.connect(bp).connect(g).connect(out);\n        src.start(t);\n      };\n\n      // round, muted \"pock\"\n      noiseBurst(420 + Math.random() * 120, 1.2, kind === \"down\" ? 0.4 : 0.15, 0.05);\n      // whisper of close-mic air\n      noiseBurst(4500 + Math.random() * 800, 1, 0.05, 0.06);\n\n      if (kind === \"down\") {\n        // deep, slow thock with a gentle pitch drop\n        const thock = ctx.createOscillator();\n        thock.type = \"sine\";\n        const freq = 95 + Math.random() * 20;\n        thock.frequency.setValueAtTime(freq, t);\n        thock.frequency.exponentialRampToValueAtTime(freq * 0.65, t + 0.12);\n        const g = ctx.createGain();\n        g.gain.setValueAtTime(0.0001, t);\n        g.gain.linearRampToValueAtTime(0.5, t + 0.006);\n        g.gain.exponentialRampToValueAtTime(0.001, t + 0.12);\n        thock.connect(g).connect(out);\n        thock.start(t);\n        thock.stop(t + 0.12);\n      }\n    },\n    [enabled],\n  );\n}\n\ninterface KeyboardProps {\n  className?: string;\n  /**\n   * Highlight keys while the user types on their physical keyboard.\n   * @default true\n   */\n  listen?: boolean;\n  /**\n   * Play a synthesized mechanical click on every key press.\n   * @default true\n   */\n  sound?: boolean;\n  /**\n   * Called with the key's value when an on-screen key is clicked.\n   * Single characters for printable keys (\"a\", \"1\", \" \"), names for the\n   * rest (\"Backspace\", \"Enter\", \"Shift\", …).\n   */\n  onKeyPress?: (key: string) => void;\n}\n\nexport function Keyboard({\n  className,\n  listen = true,\n  sound = true,\n  onKeyPress,\n}: KeyboardProps) {\n  const [pressed, setPressed] = React.useState<Set<string>>(new Set());\n  const playKey = useKeySound(sound);\n\n  React.useEffect(() => {\n    if (!listen) return;\n    const down = (e: KeyboardEvent) => {\n      if (e.repeat) return;\n      setPressed((prev) => new Set(prev).add(e.code));\n      playKey(\"down\");\n    };\n    const up = (e: KeyboardEvent) => {\n      playKey(\"up\");\n      setPressed((prev) => {\n        const next = new Set(prev);\n        next.delete(e.code);\n        return next;\n      });\n    };\n    // macOS swallows keyup while ⌘ is held; clearing on blur avoids stuck keys\n    const clear = () => setPressed(new Set());\n    window.addEventListener(\"keydown\", down);\n    window.addEventListener(\"keyup\", up);\n    window.addEventListener(\"blur\", clear);\n    return () => {\n      window.removeEventListener(\"keydown\", down);\n      window.removeEventListener(\"keyup\", up);\n      window.removeEventListener(\"blur\", clear);\n    };\n  }, [listen, playKey]);\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full max-w-4xl select-none flex-col gap-1.5 rounded-xl border border-border bg-muted/60 p-2 shadow-md sm:gap-2.5 sm:rounded-2xl sm:p-3.5\",\n        className,\n      )}\n    >\n      {ROWS.map((row, i) => (\n        <div key={i} className=\"flex gap-1.5 sm:gap-2.5\">\n          {row.map((key) => {\n            const isPressed = pressed.has(key.code);\n            return (\n              <button\n                key={key.code}\n                type=\"button\"\n                aria-label={key.ariaLabel}\n                data-pressed={isPressed || undefined}\n                onClick={() => {\n                  playKey(\"down\");\n                  onKeyPress?.(key.value ?? key.label.toLowerCase());\n                }}\n                style={{ flexGrow: key.width ?? 1, flexBasis: 0 }}\n                className={cn(\n                  \"flex h-9 min-w-0 flex-col items-center justify-center rounded-md border border-b-[3px] font-medium sm:h-12 sm:rounded-lg\",\n                  key.accent\n                    ? \"border-foreground/90 border-b-black/40 bg-foreground/90 text-background\"\n                    : \"border-border border-b-foreground/25 bg-background text-foreground\",\n                  (key.value ?? key.label).length > 1\n                    ? \"text-[8px] sm:text-[11px]\"\n                    : \"text-[10px] sm:text-sm\",\n                  \"motion-safe:transition-[transform,border-width,filter] motion-safe:duration-100 motion-safe:ease-out\",\n                  \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                  \"active:translate-y-0.5 active:border-b active:brightness-90\",\n                  \"data-pressed:translate-y-0.5 data-pressed:border-b data-pressed:brightness-90\",\n                )}\n              >\n                {key.sublabel && (\n                  <span\n                    className={cn(\n                      \"hidden text-[9px] leading-none sm:block\",\n                      key.accent ? \"text-background/60\" : \"text-muted-foreground\",\n                    )}\n                  >\n                    {key.sublabel}\n                  </span>\n                )}\n                <span className=\"leading-none\">{key.label}</span>\n              </button>\n            );\n          })}\n        </div>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}