Componentpickers
HotkeyRecorder
Record keyboard hotkey combinations with visual chip display
Preview
Live Preview
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add hotkey-recorderOr install the full npm package:
npm install bigbulluiUsage
import { HotkeyRecorder } from "@/components/ui/hotkey-recorder";
<HotkeyRecorder defaultValue="Ctrl+Shift+Z" onValueChange={(v) => {}} />Source code
hotkey-recorder.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface HotkeyRecorderProps {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
className?: string;
}
export function HotkeyRecorder({
value: controlledValue,
defaultValue = "",
onValueChange,
className,
}: HotkeyRecorderProps) {
const [internalValue, setInternalValue] = React.useState(defaultValue);
const [recorded, setRecorded] = React.useState("");
const [isListening, setIsListening] = React.useState(false);
const [disabled, setDisabled] = React.useState(false);
const isControlled = controlledValue !== undefined;
const currentValue = isControlled ? controlledValue : internalValue;
const handleKeyDown = (e: React.KeyboardEvent) => {
if (isListening || disabled) return;
// Don't capture modifier keys alone or special keys that aren't useful
if (e.key === "Control" || e.key === "Alt" || e.key === "Shift" || e.key === "Meta") {
return;
}
setIsListening(true);
const modifiers: string[] = [];
const keys: string[] = [];
const capture = (event: KeyboardEvent) => {
if (event.key === "Control") {
modifiers.push("Ctrl");
} else if (event.key === "Alt") {
modifiers.push("Alt");
} else if (event.key === "Shift") {
modifiers.push("Shift");
} else if (event.key === " ") {
keys.push("Space");
} else if (event.key.length === 1) {
keys.push(event.key);
} else {
// Special keys like ArrowUp, Enter, etc.
keys.push(event.key);
}
// Build the combo string
const combo = [...modifiers, ...keys].join("+");
setRecorded(combo);
setInternalValue(combo);
setIsListening(false);
document.removeEventListener("keydown", capture);
document.removeEventListener("keyup", endCapture);
};
const endCapture = () => {
setIsListening(false);
document.removeEventListener("keydown", capture);
document.removeEventListener("keyup", endCapture);
};
document.addEventListener("keydown", capture);
document.addEventListener("keyup", endCapture);
};
const clearCombo = () => {
setRecorded("");
setInternalValue("");
setDisabled(false);
onValueChange?.("");
};
const classes = cn(
"rounded-md border border-input bg-transparent px-3 py-2 font-mono text-sm transition-colors",
!isListening && "ring-2 ring-ring focus-visible:outline-none focus-visible:ring-2",
isListening && "opacity-50 cursor-not-allowed",
className,
);
const buttonClass = cn(
"flex items-center gap-2 rounded-md border border-transparent bg-secondary px-3 py-1.5 font-mono text-[10px] uppercase tracking-[0.15em] text-secondary-foreground",
isListening && "bg-accent text-accent-foreground",
!isListening && "hover:bg-accent/20 transition-colors"
);
return (
<div className={cn("space-y-2", className)}>
<div>
<button
onClick={() => setIsListening(true)}
onMouseDown={(e) => e.preventDefault()}
className={buttonClass}
>
{isListening ? (
<>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="8" y1="6" x2="8" y2="14" />
<line x1="12" y1="6" x2="12" y2="14" />
<line x1="16" y1="6" x2="16" y2="14" />
</svg>
Recording...
</>
) : (
"Record hotkey"
)}
</button>
{recorded && (
<div className="mt-1 flex items-center gap-1.5">
{recorded.split("+").map((part, idx) => (
<span
key={idx}
className="font-mono text-[10px] uppercase tracking-[0.15em] text-accent-foreground"
>
{part}
</span>
))}
</div>
)}
</div>
{!isListening && (
<button
onClick={clearCombo}
className="mt-1 flex items-center gap-1.5 px-2 py-1 rounded-md border border-transparent bg-destructive text-[10px] uppercase tracking-[0.15em] text-destructive hover:bg-destructive/90 transition-colors"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="18" y2="18" />
<line x1="6" y1="18" x2="18" y2="18" />
</svg>
Clear
</button>
)}
</div>
);
}
HotkeyRecorder.displayName = "HotkeyRecorder";
Props
| Prop | Type | Description |
|---|---|---|
| value | string | undefined | Controlled hotkey string |
| defaultValue | string | Default hotkey |
| onValueChange | (value: string) => void | Change handler |