{ "$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(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>(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 \n {ROWS.map((row, i) => (\n
\n {row.map((key) => {\n const isPressed = pressed.has(key.code);\n return (\n {\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 \n {key.sublabel}\n \n )}\n {key.label}\n \n );\n })}\n
\n ))}\n \n );\n}\n", "type": "registry:component" } ], "type": "registry:component" }