/**
 * Gesture System v2 — Interactive Test Page
 *
 * Full-featured test harness for the v2 shared input pipeline:
 * - Left panel: graph controls, gesture config, binding composer, event composer
 * - Center: canvas with draggable nodes
 * - Right panel: real-time event log (pointer + keyboard)
 *
 * Access via: add #v2 to the URL hash
 */

import React, { useRef, useState, useCallback, useEffect } from 'react';
import { Provider as JotaiProvider, useAtom, useSetAtom, useAtomValue } from 'jotai';
import { useGesture } from '@use-gesture/react';
import Graph from 'graphology';
import {
	gesturesV2,
	graphAtom,
	graphUpdateVersionAtom,
	nodePositionUpdateCounterAtom,
	panAtom,
	zoomAtom,
	viewportRectAtom,
	selectedNodeIdsAtom,
	clearSelectionAtom,
	handleNodePointerDownSelectionAtom,
	screenToWorldAtom,
	nodePositionAtomFamily,
	CanvasStyleProvider,
	type GraphNodeAttributes,
	type GraphEdgeAttributes,
} from '@blinksgg/canvas';

const {
	useCanvasGestures,
	useGestureSystem,
	useNodeGestures,
	registerAction,
	clearHandlers,
	resolve,
	dispatch,
	DEFAULT_CONTEXT,
	PALM_REJECTION_CONTEXT,
	PICK_NODE_CONTEXT,
	PICK_POINT_CONTEXT,
	KEYBOARD_MANIPULATE_CONTEXT,
	KEYBOARD_NAVIGATE_CONTEXT,
	SEARCH_CONTEXT,
	ACTIVE_INTERACTION_CONTEXT,
	isKeyInputEvent,
	NO_HELD_KEYS,
	NO_MODIFIERS,
} = gesturesV2;

type GestureEvent = gesturesV2.GestureEvent;
type KeyInputEvent = gesturesV2.KeyInputEvent;
type InputEvent = gesturesV2.InputEvent;
type ResolvedAction = gesturesV2.ResolvedAction;
type MappingIndex = gesturesV2.MappingIndex;
type MappingContext = gesturesV2.MappingContext;
type Binding = gesturesV2.Binding;
type GestureType = gesturesV2.GestureType;
type SubjectKind = gesturesV2.SubjectKind;
type HeldKeysState = gesturesV2.HeldKeysState;

// =============================================================================
// Constants
// =============================================================================

const COLORS = ['#6366f1', '#f97316', '#22c55e', '#3b82f6', '#ec4899'];
const GESTURE_TYPES: GestureType[] = ['tap', 'double-tap', 'triple-tap', 'long-press', 'drag', 'pinch', 'scroll'];
const SUBJECT_KINDS: SubjectKind[] = ['background', 'node', 'edge'];
const INPUT_SOURCES = ['mouse', 'finger', 'pencil'] as const;
const BUTTONS = [0, 1, 2] as const;

/** Common keys for the key event composer */
const COMMON_KEYS = [
	'Escape', 'Enter', 'Tab', 'Space', 'Backspace', 'Delete',
	'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
	'a', 'c', 'd', 'f', 'g', 'm', 'v', 'x', 'y', 'z',
	'/', '1', '2', '3',
];

const COMMON_CODES = [
	'Escape', 'Enter', 'Tab', 'Space', 'Backspace', 'Delete',
	'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
	'KeyA', 'KeyC', 'KeyD', 'KeyF', 'KeyG', 'KeyM', 'KeyV', 'KeyX', 'KeyY', 'KeyZ',
	'Slash', 'Digit1', 'Digit2', 'Digit3',
];

// =============================================================================
// Styles
// =============================================================================

const S = {
	panel: { background: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' },
	section: { padding: '10px 14px', borderBottom: '1px solid #1e293b' },
	sectionTitle: { fontSize: 10, fontWeight: 700, textTransform: 'uppercase' as const, letterSpacing: '0.08em', color: '#64748b', marginBottom: 6 },
	label: { fontSize: 11, color: '#94a3b8' },
	value: { fontSize: 11, fontFamily: 'monospace', color: '#e2e8f0' },
	btn: (active = false) => ({
		padding: '3px 8px', fontSize: 10, fontWeight: 500, borderRadius: 4, cursor: 'pointer',
		border: `1px solid ${active ? '#6366f1' : '#334155'}`,
		background: active ? '#6366f130' : '#1e293b',
		color: active ? '#a5b4fc' : '#94a3b8',
	}),
	btnPrimary: { padding: '4px 10px', fontSize: 10, fontWeight: 600, borderRadius: 4, cursor: 'pointer', border: '1px solid #6366f1', background: '#6366f1', color: '#fff' },
	btnDanger: { padding: '3px 8px', fontSize: 10, fontWeight: 500, borderRadius: 4, cursor: 'pointer', border: '1px solid #f43f5e', background: '#f43f5e20', color: '#f43f5e' },
	select: { padding: '3px 6px', fontSize: 10, borderRadius: 4, border: '1px solid #334155', background: '#1e293b', color: '#e2e8f0', cursor: 'pointer' },
	input: { padding: '3px 6px', fontSize: 10, borderRadius: 4, border: '1px solid #334155', background: '#1e293b', color: '#e2e8f0', width: 50 },
	row: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 },
	badge: (color: string) => ({ padding: '1px 6px', fontSize: 9, fontWeight: 600, borderRadius: 3, background: `${color}20`, color, display: 'inline-block' }),
};

// =============================================================================
// Graph factory
// =============================================================================

let nodeCounter = 0;

function makeGraph(count: number) {
	const g = new Graph<GraphNodeAttributes, GraphEdgeAttributes>({ multi: true, allowSelfLoops: true, type: 'directed' });
	const now = new Date().toISOString();
	const cols = Math.ceil(Math.sqrt(count));
	nodeCounter = count;

	for (let i = 0; i < count; i++) {
		const col = i % cols, row = Math.floor(i / cols);
		const x = col * 220 + 50;
		const y = row * 140 + 50;
		const color = COLORS[i % COLORS.length];
		const id = `n${i}`;
		g.addNode(id, {
			x, y, size: 10, width: 160, height: 80, color, label: `Node ${i}`, zIndex: 0,
			dbData: {
				id, graph_id: 'demo-v2', label: `Node ${i}`, node_type: 'default', configuration: null,
				ui_properties: { x, y, width: 160, height: 80, color }, created_at: now, updated_at: now
			}
		});
	}

	for (let i = 0; i < count - 1; i++) {
		const eid = `e${i}`;
		g.addEdgeWithKey(eid, `n${i}`, `n${i + 1}`, {
			color: COLORS[i % COLORS.length],
			dbData: {
				id: eid, graph_id: 'demo-v2', source_node_id: `n${i}`, target_node_id: `n${i + 1}`,
				label: null, edge_type: 'default', configuration: null, filter_condition: null, ui_properties: null, created_at: now, updated_at: now
			},
		} as unknown as GraphEdgeAttributes);
	}

	return g;
}

// =============================================================================
// Event Log
// =============================================================================

interface LogEntry {
	id: number;
	timestamp: number;
	inputKind: 'pointer' | 'key';
	type: string;
	phase: string;
	action: string;
	subject: string;
	score: number;
	source: string;
	detail?: string; // key/code for keyboard events
}

let logId = 0;

function eventToLogEntry(event: InputEvent, resolution: ResolvedAction | null): LogEntry {
	const isKey = isKeyInputEvent(event);
	return {
		id: logId++,
		timestamp: Date.now(),
		inputKind: isKey ? 'key' : 'pointer',
		type: isKey ? `key:${event.key}` : event.type,
		phase: event.phase,
		action: resolution?.actionId ?? '(no match)',
		subject: isKey
			? (event.subject?.kind ?? 'canvas')
			: event.subject.kind === 'node' ? `node:${event.subject.nodeId}`
			: event.subject.kind === 'edge' ? `edge:${event.subject.edgeId}`
			: event.subject.kind,
		score: resolution?.score ?? 0,
		source: isKey ? 'keyboard' : event.source,
		detail: isKey ? `${event.key} (${event.code})` : undefined,
	};
}

// =============================================================================
// Draggable Node
// =============================================================================

const DraggableNode = React.memo(function DraggableNode({ id, w, h, label, color, isSelected, mappingIndex, onAction }: {
	id: string; w: number; h: number;
	label: string; color: string; isSelected: boolean;
	mappingIndex: MappingIndex;
	onAction?: (event: InputEvent, resolution: ResolvedAction) => void;
}) {
	const graph = useAtomValue(graphAtom);
	const zoom = useAtomValue(zoomAtom);
	const bumpPos = useSetAtom(nodePositionUpdateCounterAtom);
	const setHandleNodePointerDown = useSetAtom(handleNodePointerDownSelectionAtom);
	const position = useAtomValue(nodePositionAtomFamily(id));
	const initialPosRef = useRef({ x: 0, y: 0 });

	const gestureHandlers = useNodeGestures({ nodeId: id, mappingIndex, onAction });

	const bind = useGesture(
		{
			onDrag: ({ movement: [mx, my], first, tap, active, pinching, event }) => {
				if (tap || !active || pinching) return;
				event.stopPropagation();
				if (first && graph.hasNode(id)) {
					const attrs = graph.getNodeAttributes(id);
					initialPosRef.current = { x: attrs.x, y: attrs.y };
				}
				const newX = initialPosRef.current.x + mx / zoom;
				const newY = initialPosRef.current.y + my / zoom;
				if (graph.hasNode(id)) {
					graph.setNodeAttribute(id, 'x', newX);
					graph.setNodeAttribute(id, 'y', newY);
					bumpPos((c: number) => c + 1);
				}
			},
		},
		{
			eventOptions: { passive: false, capture: false },
			drag: { pointer: { touch: true, keys: false, capture: false, buttons: -1 }, filterTaps: true, threshold: 3 },
		},
	);

	const bindGestures = bind();

	const handlePointerDown = (e: React.PointerEvent) => {
		gestureHandlers.onPointerDown(e);
		if (bindGestures.onPointerDown) {
			(bindGestures.onPointerDown as (e: React.PointerEvent) => void)(e);
		}
		e.stopPropagation();
		setHandleNodePointerDown({ nodeId: id, isShiftPressed: e.shiftKey });
	};

	return (
		<div
			data-draggable-node="true"
			data-node-id={id}
			{...bindGestures}
			onPointerDown={handlePointerDown}
			onPointerUp={(e: React.PointerEvent) => gestureHandlers.onPointerUp(e)}
			onPointerCancel={gestureHandlers.onPointerCancel}
			onContextMenu={gestureHandlers.onContextMenu}
			style={{
				position: 'absolute', left: position.x, top: position.y, width: w, height: h,
				background: isSelected ? `${color}40` : '#1e293b',
				border: `2px solid ${isSelected ? color : '#334155'}`,
				borderRadius: 8, display: 'flex', flexDirection: 'column',
				alignItems: 'center', justifyContent: 'center',
				cursor: 'grab', userSelect: 'none', touchAction: 'none',
				transition: 'border-color 0.15s, background 0.15s',
			}}
		>
			<span style={{ fontSize: 13, fontWeight: 600, color: '#e2e8f0' }}>{label}</span>
			<span style={{ fontSize: 10, color: '#94a3b8', fontFamily: 'monospace' }}>
				{Math.round(position.x)}, {Math.round(position.y)}
			</span>
		</div>
	);
});

// =============================================================================
// Left Panel — Configuration
// =============================================================================

function LeftPanel({
	nodeCount, onSetNodes, gestureSystem, onFireEvent,
}: {
	nodeCount: number;
	onSetNodes: (n: number) => void;
	gestureSystem: ReturnType<typeof useGestureSystem>;
	onFireEvent: (event: InputEvent) => void;
}) {
	const [pan, setPan] = useAtom(panAtom);
	const [zoom, setZoom] = useAtom(zoomAtom);
	const selectedNodeIds = useAtomValue(selectedNodeIdsAtom);

	return (
		<div style={{ width: 300, borderRight: '1px solid #1e293b', display: 'flex', flexDirection: 'column', overflow: 'auto', ...S.panel }}>
			{/* Header */}
			<div style={{ ...S.section, borderBottom: '1px solid #334155' }}>
				<div style={{ fontSize: 10, fontFamily: 'monospace', color: '#64748b' }}>shared-input-system-v2</div>
				<div style={{ fontSize: 15, fontWeight: 700, marginTop: 2 }}>Test Harness</div>
			</div>

			{/* Viewport Stats */}
			<div style={S.section}>
				<div style={S.sectionTitle}>Viewport</div>
				<div style={S.row}><span style={S.label}>Zoom</span><span style={S.value}>{(zoom * 100).toFixed(0)}%</span></div>
				<div style={S.row}><span style={S.label}>Pan</span><span style={S.value}>{Math.round(pan.x)}, {Math.round(pan.y)}</span></div>
				<div style={S.row}><span style={S.label}>Selected</span><span style={S.value}>{selectedNodeIds.size}</span></div>
				<div style={{ display: 'flex', gap: 4, marginTop: 4 }}>
					<button style={S.btn()} onClick={() => { setPan({ x: 0, y: 0 }); setZoom(1); }}>Reset View</button>
					<button style={S.btn()} onClick={() => setZoom(Math.min(5, zoom * 1.5))}>Zoom +</button>
					<button style={S.btn()} onClick={() => setZoom(Math.max(0.1, zoom / 1.5))}>Zoom -</button>
				</div>
			</div>

			{/* Graph Controls */}
			<div style={S.section}>
				<div style={S.sectionTitle}>Graph</div>
				<div style={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
					{[4, 8, 16, 25, 50].map(n => (
						<button key={n} style={S.btn(nodeCount === n)} onClick={() => onSetNodes(n)}>{n}</button>
					))}
				</div>
			</div>

			{/* Gesture Config */}
			<div style={S.section}>
				<div style={S.sectionTitle}>Input Config</div>
				<div style={S.row}>
					<button
						style={S.btn(gestureSystem.palmRejection)}
						onClick={() => gestureSystem.setPalmRejection(!gestureSystem.palmRejection)}
					>
						Palm Rejection: {gestureSystem.palmRejection ? 'ON' : 'OFF'}
					</button>
				</div>
				<HeldKeysIndicator heldKeys={gestureSystem.heldKeys} onClear={gestureSystem.clearHeldKeys} />
			</div>

			{/* Binding Composer */}
			<BindingComposer gestureSystem={gestureSystem} />

			{/* Event Composer */}
			<EventComposer onFireEvent={onFireEvent} heldKeys={gestureSystem.heldKeys} />

			{/* Controls Reference */}
			<div style={{ ...S.section, marginTop: 'auto', borderTop: '1px solid #334155', borderBottom: 'none' }}>
				<div style={S.sectionTitle}>Controls</div>
				<div style={{ fontSize: 10, color: '#64748b', lineHeight: 1.6 }}>
					<div><b style={{ color: '#94a3b8' }}>Pointer</b></div>
					<div>Drag node = Move | Drag bg = Pan</div>
					<div>Scroll = Zoom | Tap node = Select</div>
					<div>Dbl-tap = Fit | Triple-tap = Lock</div>
					<div>Right-click = Context menu</div>
					<div>Long-press bg = Create node</div>
					<div>Shift+drag bg = Rect select</div>
					<div>Space+drag = Pan (held key)</div>
					<div style={{ marginTop: 4 }}><b style={{ color: '#94a3b8' }}>Keyboard</b></div>
					<div>/ = Command line | Esc = Cancel</div>
					<div>Del/Bksp = Delete selection</div>
					<div>Ctrl/Cmd+C/X/V = Copy/Cut/Paste</div>
					<div>Ctrl/Cmd+Z = Undo | +Shift = Redo</div>
					<div>Ctrl/Cmd+A = Select all</div>
					<div>Ctrl/Cmd+F = Search</div>
				</div>
			</div>
		</div>
	);
}

// =============================================================================
// Held Keys Indicator
// =============================================================================

function HeldKeysIndicator({ heldKeys, onClear }: { heldKeys: HeldKeysState; onClear: () => void }) {
	const activeKeys = Object.entries(heldKeys.byKey).filter(([, v]) => v).map(([k]) => k);
	if (activeKeys.length === 0) return null;
	return (
		<div style={{ ...S.row, marginTop: 4 }}>
			<span style={{ ...S.label, width: 52 }}>Held</span>
			<div style={{ display: 'flex', gap: 3, flexWrap: 'wrap', flex: 1 }}>
				{activeKeys.map(k => (
					<span key={k} style={S.badge('#14b8a6')}>{k}</span>
				))}
			</div>
			<button style={{ ...S.btnDanger, padding: '1px 5px', fontSize: 9 }} onClick={onClear}>Clear</button>
		</div>
	);
}

// =============================================================================
// Binding Composer
// =============================================================================

const KNOWN_ACTIONS = [
	// Built-in pointer actions
	'none', 'select-node', 'select-edge', 'add-to-selection', 'clear-selection',
	'delete-selected', 'fit-to-view', 'fit-all-to-view', 'center-on-node',
	'reset-viewport', 'lock-node', 'unlock-node', 'toggle-lock',
	'open-context-menu', 'apply-force-layout', 'create-node',
	'move-node', 'pan', 'zoom', 'lasso-select', 'rect-select',
	'split-node', 'group-nodes', 'merge-nodes',
	'toggle-selection', 'context-menu',
	// Keyboard actions
	'undo', 'redo', 'copy-selection', 'cut-selection', 'paste-selection',
	'duplicate-selection', 'select-all', 'merge-selection',
	'delete-selection', 'open-search', 'escape-input', 'open-command-line',
	'cancel-active-input',
	// Navigate/manipulate mode actions
	'navigate-focus-up', 'navigate-focus-down', 'navigate-focus-left', 'navigate-focus-right',
	'enter-keyboard-manipulate-mode', 'exit-keyboard-manipulate-mode',
	'activate-focused-node', 'cycle-focus-forward', 'cycle-focus-backward',
	'nudge-selection-up', 'nudge-selection-down', 'nudge-selection-left', 'nudge-selection-right',
	'nudge-selection-up-large', 'nudge-selection-down-large',
	'nudge-selection-left-large', 'nudge-selection-right-large',
	// Search actions
	'search-next-result', 'search-prev-result',
	// Demo-only
	'draw-on-node', 'freehand-draw', 'continue-edge', 'finish-edge',
	'resolve-pick-node', 'resolve-pick-point', 'cancel-pick',
];

interface DraftBinding {
	inputKind: 'pointer' | 'key';
	// Pointer fields
	type: GestureType | '';
	subjectKind: SubjectKind | '';
	source: (typeof INPUT_SOURCES)[number] | '';
	button: 0 | 1 | 2 | -1;
	// Key fields
	key: string;
	code: string;
	keyPhase: 'down' | 'up';
	// Shared
	shift: boolean;
	ctrl: boolean;
	alt: boolean;
	meta: boolean;
	customMod: string;
	// Held keys
	heldKey: string;
	// Output
	actionId: string;
	consumeInput: boolean;
}

const EMPTY_DRAFT: DraftBinding = {
	inputKind: 'pointer',
	type: '', subjectKind: '', source: '', button: -1,
	key: '', code: '', keyPhase: 'down',
	shift: false, ctrl: false, alt: false, meta: false, customMod: '',
	heldKey: '',
	actionId: 'none', consumeInput: false,
};

const CUSTOM_MODS = ['draw', 'erase', 'connect', 'select'];

let composerBindingId = 0;

function draftToBinding(draft: DraftBinding): Binding {
	const pattern: gesturesV2.InputPattern = {};

	if (draft.inputKind === 'key') {
		pattern.kind = 'key';
		pattern.phase = draft.keyPhase;
		if (draft.key) pattern.key = draft.key;
		if (draft.code) pattern.code = draft.code;
	} else {
		if (draft.type) pattern.type = draft.type as GestureType;
		if (draft.subjectKind) pattern.subjectKind = draft.subjectKind as SubjectKind;
		if (draft.source) pattern.source = draft.source as gesturesV2.InputSource;
		if (draft.button >= 0) pattern.button = draft.button as 0 | 1 | 2;
	}

	if (draft.shift || draft.ctrl || draft.alt || draft.meta || draft.customMod) {
		pattern.modifiers = {};
		if (draft.shift) pattern.modifiers.shift = true;
		if (draft.ctrl) pattern.modifiers.ctrl = true;
		if (draft.alt) pattern.modifiers.alt = true;
		if (draft.meta) pattern.modifiers.meta = true;
		if (draft.customMod) pattern.modifiers.custom = { [draft.customMod]: true };
	}

	if (draft.heldKey) {
		pattern.heldKeys = { byKey: { [draft.heldKey]: true } };
	}

	return {
		id: `composed-${composerBindingId++}`,
		pattern,
		actionId: draft.actionId,
		consumeInput: draft.consumeInput || undefined,
	};
}

function patternSummary(b: Binding): string {
	const p = b.pattern;
	const parts: string[] = [];
	if (p.kind === 'key') parts.push('KEY');
	if (p.phase && p.kind === 'key') parts.push(p.phase);
	if (p.source) parts.push(p.source);
	if (p.button !== undefined) parts.push(`btn${p.button}`);
	if (p.modifiers?.shift) parts.push('\u21E7');
	if (p.modifiers?.ctrl) parts.push('\u2303');
	if (p.modifiers?.alt) parts.push('\u2325');
	if (p.modifiers?.meta) parts.push('\u2318');
	if (p.modifiers?.custom) Object.keys(p.modifiers.custom).forEach(k => parts.push(`[${k}]`));
	if (p.heldKeys?.byKey) Object.entries(p.heldKeys.byKey).filter(([, v]) => v).forEach(([k]) => parts.push(`{${k}}`));
	if (p.key) parts.push(`"${p.key}"`);
	if (p.code) parts.push(`(${p.code})`);
	if (p.type) parts.push(p.type);
	if (p.subjectKind) parts.push(`@${p.subjectKind}`);
	return parts.join(' ') || '*';
}

function BindingComposer({ gestureSystem }: { gestureSystem: ReturnType<typeof useGestureSystem> }) {
	const [bindings, setBindings] = useState<Binding[]>([]);
	const [draft, setDraft] = useState<DraftBinding>({ ...EMPTY_DRAFT });
	const [priority, setPriority] = useState(10);
	const [isPushed, setIsPushed] = useState(false);
	const contextId = 'composer';

	const addBinding = () => {
		setBindings(prev => [...prev, draftToBinding(draft)]);
		setDraft({ ...EMPTY_DRAFT, inputKind: draft.inputKind });
		if (isPushed) {
			gestureSystem.removeContext(contextId);
			setIsPushed(false);
		}
	};

	const removeBinding = (idx: number) => {
		setBindings(prev => prev.filter((_, i) => i !== idx));
		if (isPushed) {
			gestureSystem.removeContext(contextId);
			setIsPushed(false);
		}
	};

	const pushContext = () => {
		if (bindings.length === 0) return;
		const ctx: MappingContext = { id: contextId, priority, enabled: true, bindings };
		gestureSystem.pushContext(ctx);
		setIsPushed(true);
	};

	const unpushContext = () => {
		gestureSystem.removeContext(contextId);
		setIsPushed(false);
	};

	const D = draft;
	const setD = (patch: Partial<DraftBinding>) => setDraft(prev => ({ ...prev, ...patch }));
	const isKey = D.inputKind === 'key';

	return (
		<div style={S.section}>
			<div style={S.sectionTitle}>Binding Composer</div>

			<div style={{ background: '#1e293b', borderRadius: 6, padding: 8, marginBottom: 8 }}>
				{/* Input kind toggle */}
				<div style={S.row}>
					<span style={{ ...S.label, width: 46 }}>Kind</span>
					<button style={S.btn(D.inputKind === 'pointer')} onClick={() => setD({ inputKind: 'pointer' })}>Pointer</button>
					<button style={S.btn(D.inputKind === 'key')} onClick={() => setD({ inputKind: 'key' })}>Keyboard</button>
				</div>

				{isKey ? (
					<>
						{/* Key-specific fields */}
						<div style={S.row}>
							<span style={{ ...S.label, width: 46 }}>Phase</span>
							<button style={S.btn(D.keyPhase === 'down')} onClick={() => setD({ keyPhase: 'down' })}>down</button>
							<button style={S.btn(D.keyPhase === 'up')} onClick={() => setD({ keyPhase: 'up' })}>up</button>
						</div>
						<div style={S.row}>
							<span style={{ ...S.label, width: 46 }}>Key</span>
							<select style={{ ...S.select, flex: 1 }} value={D.key} onChange={e => setD({ key: e.target.value })}>
								<option value="">any</option>
								{COMMON_KEYS.map(k => <option key={k} value={k}>{k}</option>)}
							</select>
						</div>
						<div style={S.row}>
							<span style={{ ...S.label, width: 46 }}>Code</span>
							<select style={{ ...S.select, flex: 1 }} value={D.code} onChange={e => setD({ code: e.target.value })}>
								<option value="">any</option>
								{COMMON_CODES.map(c => <option key={c} value={c}>{c}</option>)}
							</select>
						</div>
					</>
				) : (
					<>
						{/* Pointer-specific fields */}
						<div style={S.row}>
							<span style={{ ...S.label, width: 46 }}>Type</span>
							<select style={S.select} value={D.type} onChange={e => setD({ type: e.target.value as GestureType | '' })}>
								<option value="">any</option>
								{GESTURE_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
							</select>
						</div>
						<div style={S.row}>
							<span style={{ ...S.label, width: 46 }}>Subject</span>
							<select style={S.select} value={D.subjectKind} onChange={e => setD({ subjectKind: e.target.value as SubjectKind | '' })}>
								<option value="">any</option>
								{SUBJECT_KINDS.map(s => <option key={s} value={s}>{s}</option>)}
							</select>
						</div>
						<div style={S.row}>
							<span style={{ ...S.label, width: 46 }}>Source</span>
							<select style={S.select} value={D.source} onChange={e => setD({ source: e.target.value as (typeof INPUT_SOURCES)[number] | '' })}>
								<option value="">any</option>
								{INPUT_SOURCES.map(s => <option key={s} value={s}>{s}</option>)}
							</select>
							<span style={{ ...S.label, marginLeft: 4 }}>Btn</span>
							<select style={S.select} value={D.button} onChange={e => setD({ button: Number(e.target.value) as -1 | 0 | 1 | 2 })}>
								<option value={-1}>any</option>
								<option value={0}>Left</option>
								<option value={1}>Mid</option>
								<option value={2}>Right</option>
							</select>
						</div>
						{/* Held key for pointer events (e.g. Space+drag) */}
						<div style={S.row}>
							<span style={{ ...S.label, width: 46 }}>Held</span>
							<select style={S.select} value={D.heldKey} onChange={e => setD({ heldKey: e.target.value })}>
								<option value="">none</option>
								{COMMON_KEYS.map(k => <option key={k} value={k}>{k}</option>)}
							</select>
							<span style={{ fontSize: 9, color: '#475569' }}>key must be held</span>
						</div>
					</>
				)}

				{/* Shared modifier buttons */}
				<div style={S.row}>
					<span style={{ ...S.label, width: 46 }}>Mods</span>
					<button style={S.btn(D.shift)} onClick={() => setD({ shift: !D.shift })}>Shift</button>
					<button style={S.btn(D.ctrl)} onClick={() => setD({ ctrl: !D.ctrl })}>Ctrl</button>
					<button style={S.btn(D.alt)} onClick={() => setD({ alt: !D.alt })}>Alt</button>
					<button style={S.btn(D.meta)} onClick={() => setD({ meta: !D.meta })}>Meta</button>
				</div>
				<div style={S.row}>
					<span style={{ ...S.label, width: 46 }}>Custom</span>
					<select style={S.select} value={D.customMod} onChange={e => setD({ customMod: e.target.value })}>
						<option value="">none</option>
						{CUSTOM_MODS.map(m => <option key={m} value={m}>{m}</option>)}
					</select>
					<span style={{ fontSize: 9, color: '#475569' }}>toolbar button</span>
				</div>

				{/* Action selector */}
				<div style={S.row}>
					<span style={{ ...S.label, width: 46 }}>Action</span>
					<select style={{ ...S.select, flex: 1 }} value={D.actionId} onChange={e => setD({ actionId: e.target.value })}>
						{KNOWN_ACTIONS.map(a => <option key={a} value={a}>{a}</option>)}
					</select>
				</div>

				{/* Flags */}
				<div style={S.row}>
					<span style={{ ...S.label, width: 46 }}>Flags</span>
					<button style={S.btn(D.consumeInput)} onClick={() => setD({ consumeInput: !D.consumeInput })}>
						consumeInput
					</button>
					<span style={{ fontSize: 9, color: '#475569' }} title="When ON, this binding blocks lower-priority contexts from matching the same event">?</span>
				</div>
				{D.consumeInput && (
					<div style={{ fontSize: 9, color: '#eab308', marginTop: -2, marginBottom: 2, paddingLeft: 52 }}>
						Blocks lower-priority contexts from seeing this event
					</div>
				)}
				<button style={{ ...S.btnPrimary, width: '100%', marginTop: 4 }} onClick={addBinding}>
					+ Add Binding
				</button>
			</div>

			{/* Composed bindings list */}
			{bindings.length > 0 && (
				<div style={{ marginBottom: 8 }}>
					<div style={{ fontSize: 10, color: '#64748b', marginBottom: 4 }}>
						Bindings ({bindings.length})
					</div>
					{bindings.map((b, i) => (
						<div key={b.id} style={{
							fontSize: 10, fontFamily: 'monospace', padding: '3px 4px',
							display: 'flex', gap: 4, alignItems: 'center',
							background: '#0f172a', borderRadius: 4, marginBottom: 2,
						}}>
							<span style={{ color: b.pattern.kind === 'key' ? '#14b8a6' : '#94a3b8', flex: 1 }}>
								{patternSummary(b)}
							</span>
							<span style={{ color: '#a78bfa' }}>{b.actionId}</span>
							{b.consumeInput && <span style={S.badge('#eab308')}>C</span>}
							<button
								style={{ ...S.btnDanger, padding: '1px 5px', fontSize: 9, lineHeight: 1 }}
								onClick={() => removeBinding(i)}
							>
								x
							</button>
						</div>
					))}
				</div>
			)}

			{/* Priority + Push/Remove */}
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Priority</span>
				<input
					type="number" min={0} max={200}
					style={{ ...S.input, width: 40 }}
					value={priority}
					onChange={e => {
						setPriority(Number(e.target.value));
						if (isPushed) { gestureSystem.removeContext(contextId); setIsPushed(false); }
					}}
				/>
				<span style={{ fontSize: 9, color: '#475569', marginLeft: 2 }}>lower = higher priority</span>
			</div>
			<div style={{ display: 'flex', gap: 4, marginTop: 4 }}>
				{!isPushed ? (
					<button
						style={{ ...S.btnPrimary, flex: 1, opacity: bindings.length === 0 ? 0.4 : 1 }}
						onClick={pushContext}
						disabled={bindings.length === 0}
					>
						Push Context ({bindings.length} bindings)
					</button>
				) : (
					<button style={{ ...S.btnDanger, flex: 1 }} onClick={unpushContext}>
						Remove Pushed Context
					</button>
				)}
			</div>

			{isPushed && (
				<div style={{ marginTop: 6, padding: '4px 8px', borderRadius: 4, background: '#6366f110', border: '1px solid #6366f140' }}>
					<div style={{ fontSize: 9, color: '#a5b4fc' }}>
						Active on stack at priority {priority} — use Event Composer to test resolution
					</div>
				</div>
			)}
		</div>
	);
}

// =============================================================================
// Event Composer — Pointer + Keyboard
// =============================================================================

function EventComposer({ onFireEvent, heldKeys }: { onFireEvent: (event: InputEvent) => void; heldKeys: HeldKeysState }) {
	const [mode, setMode] = useState<'pointer' | 'key'>('pointer');

	return (
		<div style={S.section}>
			<div style={S.sectionTitle}>Event Composer</div>
			<div style={{ ...S.row, marginBottom: 8 }}>
				<button style={S.btn(mode === 'pointer')} onClick={() => setMode('pointer')}>Pointer</button>
				<button style={S.btn(mode === 'key')} onClick={() => setMode('key')}>Keyboard</button>
			</div>
			{mode === 'pointer'
				? <PointerEventComposer onFireEvent={onFireEvent} heldKeys={heldKeys} />
				: <KeyEventComposer onFireEvent={onFireEvent} />
			}
		</div>
	);
}

function PointerEventComposer({ onFireEvent, heldKeys }: { onFireEvent: (event: InputEvent) => void; heldKeys: HeldKeysState }) {
	const [type, setType] = useState<GestureType>('tap');
	const [subjectKind, setSubjectKind] = useState<SubjectKind>('background');
	const [nodeId, setNodeId] = useState('n0');
	const [source, setSource] = useState<(typeof INPUT_SOURCES)[number]>('mouse');
	const [button, setButton] = useState<0 | 1 | 2>(0);
	const [shift, setShift] = useState(false);
	const [ctrl, setCtrl] = useState(false);
	const [alt, setAlt] = useState(false);
	const [meta, setMeta] = useState(false);
	const [customMod, setCustomMod] = useState('');
	const [heldKey, setHeldKey] = useState('');
	const screenToWorld = useAtomValue(screenToWorldAtom);

	const fire = () => {
		const worldPos = screenToWorld(400, 300);
		const modifiers: gesturesV2.Modifiers = { shift, ctrl, alt, meta };
		if (customMod) modifiers.custom = { [customMod]: true };

		// Merge manual held key with system held keys
		let eventHeldKeys = heldKeys;
		if (heldKey) {
			eventHeldKeys = {
				byKey: { ...heldKeys.byKey, [heldKey]: true },
				byCode: { ...heldKeys.byCode },
			};
		}

		const event: GestureEvent = {
			type,
			phase: 'instant',
			subject: subjectKind === 'node' ? { kind: 'node', nodeId }
				: subjectKind === 'edge' ? { kind: 'edge', edgeId: 'e0' }
				: { kind: 'background' },
			source,
			button,
			modifiers,
			heldKeys: eventHeldKeys,
			screenPosition: { x: 400, y: 300 },
			worldPosition: worldPos,
		};
		onFireEvent(event);
	};

	return (
		<>
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Type</span>
				<select style={S.select} value={type} onChange={e => setType(e.target.value as GestureType)}>
					{GESTURE_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
				</select>
			</div>
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Subject</span>
				<select style={S.select} value={subjectKind} onChange={e => setSubjectKind(e.target.value as SubjectKind)}>
					{SUBJECT_KINDS.map(s => <option key={s} value={s}>{s}</option>)}
				</select>
				{subjectKind === 'node' && (
					<input style={S.input} value={nodeId} onChange={e => setNodeId(e.target.value)} placeholder="n0" />
				)}
			</div>
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Source</span>
				<select style={S.select} value={source} onChange={e => setSource(e.target.value as typeof source)}>
					{INPUT_SOURCES.map(s => <option key={s} value={s}>{s}</option>)}
				</select>
				<span style={{ ...S.label, marginLeft: 4 }}>Btn</span>
				<select style={S.select} value={button} onChange={e => setButton(Number(e.target.value) as 0 | 1 | 2)}>
					{BUTTONS.map(b => <option key={b} value={b}>{b === 0 ? 'Left' : b === 1 ? 'Mid' : 'Right'}</option>)}
				</select>
			</div>
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Mods</span>
				<button style={S.btn(shift)} onClick={() => setShift(!shift)}>Shift</button>
				<button style={S.btn(ctrl)} onClick={() => setCtrl(!ctrl)}>Ctrl</button>
				<button style={S.btn(alt)} onClick={() => setAlt(!alt)}>Alt</button>
				<button style={S.btn(meta)} onClick={() => setMeta(!meta)}>Meta</button>
			</div>
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Custom</span>
				<select style={S.select} value={customMod} onChange={e => setCustomMod(e.target.value)}>
					<option value="">none</option>
					{CUSTOM_MODS.map(m => <option key={m} value={m}>{m}</option>)}
				</select>
			</div>
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Held</span>
				<select style={S.select} value={heldKey} onChange={e => setHeldKey(e.target.value)}>
					<option value="">none</option>
					{COMMON_KEYS.map(k => <option key={k} value={k}>{k}</option>)}
				</select>
				<span style={{ fontSize: 9, color: '#475569' }}>simulated held key</span>
			</div>
			<button style={{ ...S.btnPrimary, width: '100%', marginTop: 6 }} onClick={fire}>
				Fire Pointer Event
			</button>
		</>
	);
}

function KeyEventComposer({ onFireEvent }: { onFireEvent: (event: InputEvent) => void }) {
	const [key, setKey] = useState('Escape');
	const [code, setCode] = useState('Escape');
	const [phase, setPhase] = useState<'down' | 'up'>('down');
	const [shift, setShift] = useState(false);
	const [ctrl, setCtrl] = useState(false);
	const [alt, setAlt] = useState(false);
	const [meta, setMeta] = useState(false);

	const fire = () => {
		const event: KeyInputEvent = {
			kind: 'key',
			phase,
			key,
			code,
			repeat: false,
			modifiers: { shift, ctrl, alt, meta },
			heldKeys: NO_HELD_KEYS,
		};
		onFireEvent(event);
	};

	// Sync code when key changes (best guess)
	const handleKeyChange = (k: string) => {
		setKey(k);
		const idx = COMMON_KEYS.indexOf(k);
		if (idx >= 0 && idx < COMMON_CODES.length) setCode(COMMON_CODES[idx]);
	};

	return (
		<>
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Phase</span>
				<button style={S.btn(phase === 'down')} onClick={() => setPhase('down')}>down</button>
				<button style={S.btn(phase === 'up')} onClick={() => setPhase('up')}>up</button>
			</div>
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Key</span>
				<select style={{ ...S.select, flex: 1 }} value={key} onChange={e => handleKeyChange(e.target.value)}>
					{COMMON_KEYS.map(k => <option key={k} value={k}>{k}</option>)}
				</select>
			</div>
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Code</span>
				<select style={{ ...S.select, flex: 1 }} value={code} onChange={e => setCode(e.target.value)}>
					{COMMON_CODES.map(c => <option key={c} value={c}>{c}</option>)}
				</select>
			</div>
			<div style={S.row}>
				<span style={{ ...S.label, width: 46 }}>Mods</span>
				<button style={S.btn(shift)} onClick={() => setShift(!shift)}>Shift</button>
				<button style={S.btn(ctrl)} onClick={() => setCtrl(!ctrl)}>Ctrl</button>
				<button style={S.btn(alt)} onClick={() => setAlt(!alt)}>Alt</button>
				<button style={S.btn(meta)} onClick={() => setMeta(!meta)}>Meta</button>
			</div>
			<button style={{ ...S.btnPrimary, width: '100%', marginTop: 6 }} onClick={fire}>
				Fire Key Event
			</button>
		</>
	);
}

// =============================================================================
// Right Panel — Event Log
// =============================================================================

function RightPanel({ log, onClear }: { log: LogEntry[]; onClear: () => void }) {
	return (
		<div style={{ width: 360, borderLeft: '1px solid #1e293b', display: 'flex', flexDirection: 'column', ...S.panel }}>
			<div style={{ ...S.section, borderBottom: '1px solid #334155' }}>
				<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
					<div>
						<div style={{ fontSize: 13, fontWeight: 700 }}>Event Log</div>
						<div style={{ fontSize: 10, color: '#64748b' }}>{log.length} events</div>
					</div>
					<button style={S.btn()} onClick={onClear}>Clear</button>
				</div>
			</div>

			<div style={{ flex: 1, overflow: 'auto', padding: '4px 0' }}>
				{log.length === 0 && (
					<div style={{ padding: '24px 16px', textAlign: 'center', color: '#334155', fontSize: 11 }}>
						Interact with the canvas or fire<br />synthetic events to see the log.
					</div>
				)}
				{log.map(entry => (
					<div
						key={entry.id}
						style={{
							padding: '4px 12px', fontSize: 10, fontFamily: 'monospace',
							borderBottom: '1px solid #0f172a',
							display: 'flex', gap: 6, alignItems: 'center',
						}}
					>
						<InputKindBadge kind={entry.inputKind} />
						<TypeBadge type={entry.type} isKey={entry.inputKind === 'key'} />
						<span style={{ color: '#64748b', fontSize: 9, width: 32 }}>{entry.phase}</span>
						<span style={{ color: '#a78bfa', minWidth: 70 }}>{entry.action}</span>
						<span style={{ color: '#94a3b8', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
							{entry.detail ?? entry.subject}
						</span>
						<span style={{ color: '#475569', fontSize: 9 }}>{entry.source}</span>
						<span style={{ color: '#334155', fontSize: 9 }}>s:{entry.score}</span>
					</div>
				))}
			</div>
		</div>
	);
}

function InputKindBadge({ kind }: { kind: 'pointer' | 'key' }) {
	const isKey = kind === 'key';
	return (
		<span style={{
			padding: '1px 4px', fontSize: 8, fontWeight: 700, borderRadius: 2,
			background: isKey ? '#14b8a620' : '#6366f120',
			color: isKey ? '#14b8a6' : '#6366f1',
			width: 16, textAlign: 'center',
		}}>
			{isKey ? 'K' : 'P'}
		</span>
	);
}

function TypeBadge({ type, isKey }: { type: string; isKey: boolean }) {
	const pointerColors: Record<string, string> = {
		tap: '#22c55e', 'double-tap': '#3b82f6', 'triple-tap': '#a855f7',
		drag: '#f97316', 'long-press': '#f43f5e', pinch: '#eab308', scroll: '#14b8a6',
	};
	const c = isKey ? '#14b8a6' : (pointerColors[type] ?? '#94a3b8');
	return (
		<span style={{
			padding: '1px 5px', fontSize: 9, fontWeight: 600, borderRadius: 3,
			background: `${c}20`, color: c, minWidth: 52, textAlign: 'center', display: 'inline-block',
		}}>
			{type}
		</span>
	);
}

// =============================================================================
// Main V2 Canvas
// =============================================================================

function V2Canvas() {
	const [graph] = useAtom(graphAtom);
	const setGraph = useSetAtom(graphAtom);
	const bumpVer = useSetAtom(graphUpdateVersionAtom);
	const bumpPos = useSetAtom(nodePositionUpdateCounterAtom);
	const [pan] = useAtom(panAtom);
	const [zoom] = useAtom(zoomAtom);
	const selectedNodeIds = useAtomValue(selectedNodeIdsAtom);
	const clearSelection = useSetAtom(clearSelectionAtom);

	const viewportRef = useRef<HTMLDivElement>(null);
	const [log, setLog] = useState<LogEntry[]>([]);
	const [nodeCount, setNodeCount] = useState(8);

	const bump = useCallback(() => {
		bumpVer((v: number) => v + 1);
		bumpPos((c: number) => c + 1);
	}, [bumpVer, bumpPos]);

	const loadGraph = useCallback((count: number) => {
		setGraph(makeGraph(count));
		bump();
	}, [setGraph, bump]);

	useEffect(() => { loadGraph(nodeCount); }, []);

	const handleSetNodes = useCallback((n: number) => {
		setNodeCount(n);
		loadGraph(n);
	}, [loadGraph]);

	// --- Gesture system ---
	const gestureSystem = useGestureSystem({ palmRejection: true });

	// --- Register action handlers ---
	useEffect(() => {
		// Pointer actions
		registerAction('select-node', (event: InputEvent) => {
			console.log('[v2] select-node', isKeyInputEvent(event) ? 'key' : (event as GestureEvent).subject);
		});
		registerAction('clear-selection', () => { clearSelection(); });
		registerAction('pan', {
			onStart: () => console.log('[v2] pan:start'),
			onMove: () => {},
			onEnd: () => console.log('[v2] pan:end'),
		});
		registerAction('zoom', () => console.log('[v2] zoom'));
		registerAction('context-menu', (event: InputEvent) => {
			if (!isKeyInputEvent(event)) console.log('[v2] context-menu', event.subject, event.worldPosition);
		});
		registerAction('create-node', (event: InputEvent) => {
			if (isKeyInputEvent(event)) return;
			const { x, y } = event.worldPosition;
			const id = `n${nodeCounter++}`;
			const color = COLORS[Math.floor(Math.random() * COLORS.length)];
			const now = new Date().toISOString();
			graph.addNode(id, {
				x: x - 80, y: y - 40, size: 10, width: 160, height: 80, color, label: `Node ${nodeCounter - 1}`, zIndex: 0,
				dbData: {
					id, graph_id: 'demo-v2', label: `Node ${nodeCounter - 1}`, node_type: 'default', configuration: null,
					ui_properties: { x: x - 80, y: y - 40, width: 160, height: 80, color }, created_at: now, updated_at: now
				}
			});
			bump();
			setNodeCount(graph.order);
		});
		registerAction('fit-to-view', (e: InputEvent) => console.log('[v2] fit-to-view', !isKeyInputEvent(e) && e.subject));
		registerAction('toggle-lock', (e: InputEvent) => console.log('[v2] toggle-lock', !isKeyInputEvent(e) && e.subject));
		registerAction('move-node', {
			onStart: (e) => console.log('[v2] move-node:start', e.subject),
			onMove: () => {},
			onEnd: (e) => console.log('[v2] move-node:end', e.subject),
		});
		registerAction('lasso-select', {
			onStart: () => console.log('[v2] lasso:start'),
			onMove: () => {},
			onEnd: () => console.log('[v2] lasso:end'),
		});
		registerAction('rect-select', {
			onStart: () => console.log('[v2] rect:start'),
			onMove: () => {},
			onEnd: () => console.log('[v2] rect:end'),
		});
		// Node operations
		registerAction('split-node', (e: InputEvent) => console.log('[v2] split-node', !isKeyInputEvent(e) && e.subject));
		registerAction('group-nodes', () => console.log('[v2] group-nodes', selectedNodeIds));
		registerAction('merge-nodes', () => console.log('[v2] merge-nodes', selectedNodeIds));

		// Keyboard actions
		registerAction('undo', () => console.log('[v2] undo'));
		registerAction('redo', () => console.log('[v2] redo'));
		registerAction('copy-selection', () => console.log('[v2] copy-selection'));
		registerAction('cut-selection', () => console.log('[v2] cut-selection'));
		registerAction('paste-selection', () => console.log('[v2] paste-selection'));
		registerAction('duplicate-selection', () => console.log('[v2] duplicate-selection'));
		registerAction('select-all', () => console.log('[v2] select-all'));
		registerAction('merge-selection', () => console.log('[v2] merge-selection'));
		registerAction('delete-selection', () => console.log('[v2] delete-selection'));
		registerAction('open-search', () => console.log('[v2] open-search'));
		registerAction('open-command-line', () => console.log('[v2] open-command-line'));
		registerAction('escape-input', () => console.log('[v2] escape-input'));
		registerAction('cancel-active-input', () => console.log('[v2] cancel-active-input'));

		// Navigate mode
		registerAction('navigate-focus-up', () => console.log('[v2] navigate: focus up'));
		registerAction('navigate-focus-down', () => console.log('[v2] navigate: focus down'));
		registerAction('navigate-focus-left', () => console.log('[v2] navigate: focus left'));
		registerAction('navigate-focus-right', () => console.log('[v2] navigate: focus right'));
		registerAction('enter-keyboard-manipulate-mode', () => console.log('[v2] enter manipulate mode'));
		registerAction('exit-keyboard-manipulate-mode', () => console.log('[v2] exit manipulate mode'));
		registerAction('activate-focused-node', () => console.log('[v2] activate focused node'));
		registerAction('cycle-focus-forward', () => console.log('[v2] cycle focus forward'));
		registerAction('cycle-focus-backward', () => console.log('[v2] cycle focus backward'));

		// Manipulate mode
		registerAction('nudge-selection-up', () => console.log('[v2] nudge up'));
		registerAction('nudge-selection-down', () => console.log('[v2] nudge down'));
		registerAction('nudge-selection-left', () => console.log('[v2] nudge left'));
		registerAction('nudge-selection-right', () => console.log('[v2] nudge right'));
		registerAction('nudge-selection-up-large', () => console.log('[v2] nudge up (large)'));
		registerAction('nudge-selection-down-large', () => console.log('[v2] nudge down (large)'));
		registerAction('nudge-selection-left-large', () => console.log('[v2] nudge left (large)'));
		registerAction('nudge-selection-right-large', () => console.log('[v2] nudge right (large)'));

		// Search
		registerAction('search-next-result', () => console.log('[v2] search next'));
		registerAction('search-prev-result', () => console.log('[v2] search prev'));

		// Preset context actions
		registerAction('draw-on-node', (e: InputEvent) => console.log('[v2] draw-on-node'));
		registerAction('freehand-draw', (e: InputEvent) => console.log('[v2] freehand-draw'));
		registerAction('continue-edge', (e: InputEvent) => console.log('[v2] continue-edge'));
		registerAction('finish-edge', (e: InputEvent) => console.log('[v2] finish-edge'));
		registerAction('resolve-pick-node', (e: InputEvent) => console.log('[v2] resolve-pick-node'));
		registerAction('resolve-pick-point', (e: InputEvent) => console.log('[v2] resolve-pick-point'));
		registerAction('cancel-pick', () => console.log('[v2] cancel-pick'));

		return () => clearHandlers();
	}, [clearSelection, graph, bump]);

	// --- onAction callback: log every resolved gesture ---
	const addLogEntry = useCallback((event: InputEvent, resolution: ResolvedAction) => {
		setLog(prev => [eventToLogEntry(event, resolution), ...prev].slice(0, 100));
	}, []);

	// --- Fire synthetic event (from composer) ---
	const handleFireEvent = useCallback((event: InputEvent) => {
		const guardContext: gesturesV2.GuardContext = {
			isStylusActive: false, fingerCount: 0, isDragging: false,
			isResizing: false, isSplitting: false, inputMode: { type: 'normal' },
			keyboardInteractionMode: 'navigate',
			selectedNodeIds,
			focusedNodeId: null,
			isSearchActive: false,
			commandLineVisible: false,
			heldKeys: gestureSystem.heldKeys,
			custom: {},
		};
		const resolution = resolve(event, gestureSystem.mappingIndex, guardContext);
		if (resolution) {
			dispatch(event, resolution);
		}
		setLog(prev => [eventToLogEntry(event, resolution), ...prev].slice(0, 100));
	}, [gestureSystem.mappingIndex, gestureSystem.heldKeys, selectedNodeIds]);

	// --- Wire useCanvasGestures ---
	useCanvasGestures({
		ref: viewportRef,
		minZoom: 0.1, maxZoom: 5,
		enablePan: true, enableZoom: true,
		mappingIndex: gestureSystem.mappingIndex,
		onAction: addLogEntry,
	});

	// --- Render nodes ---
	const _graphVer = useAtomValue(graphUpdateVersionAtom);

	const nodes: React.ReactNode[] = [];
	graph.forEachNode((nodeId, attrs) => {
		nodes.push(
			<DraggableNode
				key={nodeId} id={nodeId}
				w={attrs.width ?? 160} h={attrs.height ?? 80}
				label={attrs.label ?? nodeId}
				color={attrs.color ?? '#6366f1'}
				isSelected={selectedNodeIds.has(nodeId)}
				mappingIndex={gestureSystem.mappingIndex}
				onAction={addLogEntry}
			/>
		);
	});

	return (
		<div style={{ display: 'flex', width: '100vw', height: '100vh', background: '#0f172a', color: '#e2e8f0' }}>
			<LeftPanel
				nodeCount={nodeCount}
				onSetNodes={handleSetNodes}
				gestureSystem={gestureSystem}
				onFireEvent={handleFireEvent}
			/>

			{/* Canvas area */}
			<div style={{ flex: 1, position: 'relative' }}>
				<div
					ref={viewportRef}
					style={{
						width: '100%', height: '100%', overflow: 'hidden', position: 'relative',
						touchAction: 'none', userSelect: 'none',
						background: 'linear-gradient(145deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%)',
					}}
				>
					<div
						style={{
							transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
							transformOrigin: '0 0', width: '100%', height: '100%',
						}}
					>
						{nodes}
					</div>
				</div>
			</div>

			<RightPanel log={log} onClear={() => setLog([])} />
		</div>
	);
}

// =============================================================================
// Export
// =============================================================================

export function GestureV2Demo() {
	return (
		<JotaiProvider>
			<CanvasStyleProvider isDark={true}>
				<V2Canvas />
			</CanvasStyleProvider>
		</JotaiProvider>
	);
}
