Componentpickers1.43 KB gzipZero dependencies
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
Open in StackBlitzimport { HotkeyRecorder } from "@/components/ui/hotkey-recorder";
<HotkeyRecorder defaultValue="Ctrl+Shift+Z" onValueChange={(v) => {}} />Usage guidance
Use HotkeyRecorder when a ticket stub surface needs Record keyboard hotkey combinations with visual chip display Keep props minimal and prefer composition with sibling stubs.
Avoid HotkeyRecorder for long form flows or dense data grids where a dedicated layout block fits better.
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 | |
| defaultValue? | string | |
| onValueChange? | (value: string) => void | |
| className? | string |