+ );
+}
+
+/**
+ * Draws the record key in its physical context: Esc above, 1 to its right, and
+ * Tab / Q on the row below — so the key is recognisable on a real keyboard.
+ */
+export function OnboardingKeyboard({
+ highlightKey = '`',
+ state,
+ progress = 0,
+}: OnboardingKeyboardProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/js/desktop/src/features/onboarding/onboarding-overlay.tsx b/js/desktop/src/features/onboarding/onboarding-overlay.tsx
new file mode 100644
index 0000000..a0d0cf1
--- /dev/null
+++ b/js/desktop/src/features/onboarding/onboarding-overlay.tsx
@@ -0,0 +1,148 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { WindowControls } from '@/components/window-controls';
+import { KeyHint } from '@/components/key-hint';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+import { OnboardingKeyboard } from './onboarding-keyboard';
+import { OnboardingStep } from './onboarding-step';
+import { HoldStep } from './hold-step';
+import { ToggleStep } from './toggle-step';
+import { TapStep } from './tap-step';
+import { PermissionStep } from './permission-step';
+
+const STEPS = [
+ 'welcome',
+ 'hold',
+ 'toggle',
+ 'tap',
+ 'permission',
+ 'done',
+] as const;
+type Step = (typeof STEPS)[number];
+
+/**
+ * First-run wizard that teaches the record key. Rendered as a full-screen
+ * takeover (above PusherProvider) so none of the stream/compose keyboard
+ * handlers are mounted to compete for the same keys.
+ */
+export function OnboardingOverlay({ onComplete }: { onComplete: () => void }) {
+ const [step, setStep] = useState('welcome');
+ const panelRef = useRef(null);
+ const index = STEPS.indexOf(step);
+
+ const goNext = useCallback(() => {
+ setStep((cur) => STEPS[Math.min(STEPS.indexOf(cur) + 1, STEPS.length - 1)]);
+ }, []);
+
+ // Focus the panel on the gesture steps so window-level key handling has a
+ // home; the static steps autofocus their primary button instead, which lets
+ // Enter activate it natively (no duplicate window handler).
+ useEffect(() => {
+ if (step === 'hold' || step === 'toggle' || step === 'tap') {
+ panelRef.current?.focus();
+ }
+ }, [step]);
+
+ // Esc skips the wizard from anywhere.
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ onComplete();
+ }
+ };
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ }, [onComplete]);
+
+ return (
+