Examples
Collection of practical examples showing how to use useNormalizedKeys in various scenarios with all the latest features.
Unified Hook API with 60fps Animations
The following examples showcase the unified useHoldSequence
hook with Context Provider to provide key info through a common context and smooth 60fps animations.
Brush Pressure with Smooth Visual Effects
tsx
import {
NormalizedKeysProvider,
useHoldSequence,
holdSequence,
Keys
} from 'use-normalized-keys';
import { useEffect } from 'react';
function BrushPressureExample() {
const brushPressure = useHoldSequence('brush-pressure');
// Trigger drawing actions
useEffect(() => {
if (brushPressure.justStarted) {
console.log('Start applying brush pressure!');
}
if (brushPressure.justCompleted) {
console.log('Maximum brush pressure applied!');
}
if (brushPressure.justCancelled) {
console.log('Brush pressure cancelled');
}
}, [brushPressure.justStarted, brushPressure.justCompleted, brushPressure.justCancelled]);
return (
<div
className="brush-pressure-indicator"
style={{
transform: `scale(${brushPressure.scale})`,
opacity: brushPressure.opacity,
boxShadow: brushPressure.glow > 0 ? `0 0 ${brushPressure.glow * 30}px #3b82f6` : 'none',
marginLeft: `${brushPressure.shake}px`,
padding: '20px',
borderRadius: '10px',
background: brushPressure.isReady ? '#3b82f6' : '#f8fafc',
color: brushPressure.isReady ? 'white' : '#1e293b',
transition: 'background-color 0.3s'
}}
>
<h3>Brush Pressure (Hold Space)</h3>
<div
className="progress-bar"
style={{
width: '200px',
height: '20px',
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: '10px',
overflow: 'hidden'
}}
>
<div style={{
width: `${brushPressure.progress}%`,
height: '100%',
background: `linear-gradient(90deg, #4CAF50 0%, #3b82f6 100%)`,
transition: 'none' // RAF handles animations!
}} />
</div>
<div>Pressure: {Math.round(brushPressure.progress)}%</div>
<div>Remaining: {brushPressure.remainingTime}ms</div>
<div>Brush Size: {Math.round(10 + brushPressure.progress / 10)}px</div>
{brushPressure.isReady && <div className="ready-indicator">🎨 MAXIMUM PRESSURE!</div>}
{brushPressure.isCharging && <div>🎨 Building pressure...</div>}
</div>
);
}
function App() {
return (
<NormalizedKeysProvider
sequences={[
holdSequence('brush-pressure', Keys.SPACE, 1000, { name: 'Brush Pressure' })
]}
preventDefault={[Keys.F5, Keys.TAB]} // Prevent specific browser shortcuts
>
<BrushPressureExample />
</NormalizedKeysProvider>
);
}
Multi-Tool Drawing Interface
tsx
import {
NormalizedKeysProvider,
useHoldSequence,
holdSequence,
Keys
} from 'use-normalized-keys';
import { useEffect, useState } from 'react';
function DrawingTools() {
const [toolActions, setToolActions] = useState<string[]>([]);
const brushPressure = useHoldSequence('brush-pressure');
const eraseStrength = useHoldSequence('erase-strength');
const panMode = useHoldSequence('pan-mode');
const eyedropper = useHoldSequence('eyedropper');
// Trigger tool actions when completed
useEffect(() => {
if (brushPressure.justCompleted) {
setToolActions(prev => [...prev, `🎨 Brush at maximum pressure applied!`]);
}
if (eraseStrength.justCompleted) {
setToolActions(prev => [...prev, `🧽 Strong eraser activated!`]);
}
if (panMode.justCompleted) {
setToolActions(prev => [...prev, `✋ Pan mode activated for ${panMode.elapsedTime}ms`]);
}
if (eyedropper.justCompleted) {
setToolActions(prev => [...prev, `💧 Color sampled with precision!`]);
}
}, [
brushPressure.justCompleted,
eraseStrength.justCompleted,
panMode.justCompleted,
eyedropper.justCompleted
]);
return (
<div className="tools-panel">
<h2>🎨 Drawing Tools Interface</h2>
<div className="tools-grid" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div className="tool" style={{
padding: '15px',
border: '2px solid #ddd',
borderRadius: '10px',
transform: `scale(${brushPressure.scale})`,
opacity: brushPressure.opacity
}}>
<h3>🎨 Brush Pressure (Space)</h3>
<div className="progress-bar" style={{
width: '100%',
height: '15px',
backgroundColor: '#eee',
borderRadius: '7px'
}}>
<div style={{
width: `${brushPressure.progress}%`,
height: '100%',
backgroundColor: '#3b82f6',
borderRadius: '7px'
}} />
</div>
<div>Size: {Math.round(5 + brushPressure.progress / 10)}px</div>
<div>Pressure: {Math.round(brushPressure.progress)}%</div>
{brushPressure.isReady && <div>🎨 MAX PRESSURE!</div>}
</div>
<div className="tool" style={{
padding: '15px',
border: '2px solid #ddd',
borderRadius: '10px',
transform: `scale(${eraseStrength.scale})`,
opacity: eraseStrength.opacity,
marginLeft: `${eraseStrength.shake}px`
}}>
<h3>🧽 Eraser (E)</h3>
<div className="progress-bar" style={{
width: '100%',
height: '15px',
backgroundColor: '#eee',
borderRadius: '7px'
}}>
<div style={{
width: `${eraseStrength.progress}%`,
height: '100%',
backgroundColor: '#ef4444',
borderRadius: '7px'
}} />
</div>
<div>Strength: {Math.round(eraseStrength.progress)}%</div>
<div>Remaining: {eraseStrength.remainingTime}ms</div>
{eraseStrength.isReady && <div>🧽 STRONG ERASE!</div>}
</div>
<div className="tool" style={{
padding: '15px',
border: '2px solid #ddd',
borderRadius: '10px',
transform: `scale(${panMode.scale})`,
opacity: panMode.opacity
}}>
<h3>✋ Pan Mode (H)</h3>
<div className="progress-bar" style={{
width: '100%',
height: '15px',
backgroundColor: '#eee',
borderRadius: '7px'
}}>
<div style={{
width: `${panMode.progress}%`,
height: '100%',
backgroundColor: '#10b981',
borderRadius: '7px'
}} />
</div>
<div>{panMode.isHolding ? 'PANNING' : 'Ready'} - {panMode.elapsedTime}ms</div>
</div>
<div className="tool" style={{
padding: '15px',
border: '2px solid #ddd',
borderRadius: '10px',
transform: `scale(${eyedropper.scale})`,
opacity: eyedropper.opacity
}}>
<h3>💧 Eyedropper (I)</h3>
<div className="progress-bar" style={{
width: '100%',
height: '15px',
backgroundColor: '#eee',
borderRadius: '7px'
}}>
<div style={{
width: `${eyedropper.progress}%`,
height: '100%',
backgroundColor: '#8b5cf6',
borderRadius: '7px'
}} />
</div>
<div>Precision: {Math.round(eyedropper.progress)}%</div>
<div>Remaining: {eyedropper.remainingTime}ms</div>
{eyedropper.isReady && <div>💧 PRECISE SAMPLE!</div>}
</div>
</div>
<div className="action-log" style={{ marginTop: '20px' }}>
<h3>Tool Actions:</h3>
<div style={{ maxHeight: '150px', overflowY: 'auto', padding: '10px', backgroundColor: '#f5f5f5' }}>
{toolActions.slice(-5).map((action, i) => (
<div key={i}>{action}</div>
))}
</div>
</div>
</div>
);
}
function App() {
return (
<NormalizedKeysProvider
sequences={[
holdSequence('brush-pressure', Keys.SPACE, 750, { name: 'Brush Pressure' }),
holdSequence('erase-strength', Keys.e, 1200, { name: 'Eraser Strength' }),
holdSequence('pan-mode', Keys.h, 500, { name: 'Pan Mode' }),
holdSequence('eyedropper', Keys.i, 800, { name: 'Eyedropper Precision' })
]}
debug={false}
tapHoldThreshold={150}
>
<DrawingTools />
</NormalizedKeysProvider>
);
}
Direct Hook Usage
For advanced use cases that need direct control over the keyboard state, you can use useNormalizedKeys
directly.
Basic Key Detection
tsx
import { useNormalizedKeys, Keys } from 'use-normalized-keys';
import { useEffect } from 'react';
function BasicKeyDetection() {
const keys = useNormalizedKeys();
// React to keyboard events
useEffect(() => {
if (keys.lastEvent?.type === 'keyup') {
console.log(`Released: ${keys.lastEvent.key} (${keys.lastEvent.duration}ms)`);
}
}, [keys.lastEvent]);
return (
<div>
<h2>Basic Key Detection</h2>
<p>Last key: {keys.lastEvent?.key || 'None'}</p>
<p>Pressed keys: {Array.from(keys.pressedKeys).join(', ') || 'None'}</p>
<p>Space pressed: {keys.isKeyPressed(Keys.SPACE) ? 'Yes' : 'No'}</p>
</div>
);
}
Productivity Shortcuts Detection
tsx
import { useNormalizedKeys, comboSequence, chordSequence, Keys } from 'use-normalized-keys';
function ProductivityShortcuts() {
const keys = useNormalizedKeys({
sequences: [
comboSequence('vim-escape', [Keys.j, Keys.k], { timeout: 300 }),
chordSequence('save', [Keys.CONTROL, Keys.s]),
chordSequence('copy', [Keys.CONTROL, Keys.c]),
chordSequence('paste', [Keys.CONTROL, Keys.v]),
chordSequence('undo', [Keys.CONTROL, Keys.z])
]
});
return (
<div>
<h2>Productivity Shortcuts</h2>
<p>Try: Vim escape (jk), Ctrl+S, Ctrl+C, Ctrl+V, or Ctrl+Z</p>
<p>Detected shortcuts: {keys.sequences?.matches.length || 0}</p>
<div>
<h3>Available Shortcuts:</h3>
<ul>
<li>jk - Vim-style escape sequence</li>
<li>Ctrl+S - Save document</li>
<li>Ctrl+C - Copy selection</li>
<li>Ctrl+V - Paste content</li>
<li>Ctrl+Z - Undo action</li>
</ul>
</div>
</div>
);
}
Hold Detection for Progressive Actions
tsx
import { useNormalizedKeys, Keys } from 'use-normalized-keys';
import { useState, useEffect } from 'react';
function TextEditorHoldExample() {
const [scrollSpeed, setScrollSpeed] = useState(0);
const [editorActions, setEditorActions] = useState<string[]>([]);
const keys = useNormalizedKeys({
sequences: [
{
id: 'slow-scroll',
name: 'Slow Scroll',
keys: [{ key: Keys.SPACE, minHoldTime: 300 }],
type: 'hold'
},
{
id: 'medium-scroll',
name: 'Medium Scroll',
keys: [{ key: Keys.SPACE, minHoldTime: 700 }],
type: 'hold'
},
{
id: 'fast-scroll',
name: 'Fast Scroll',
keys: [{ key: Keys.SPACE, minHoldTime: 1200 }],
type: 'hold'
},
{
id: 'format-selection',
name: 'Format Selection',
keys: [{
key: Keys.f,
minHoldTime: 800,
modifiers: { shift: true }
}],
type: 'hold'
}
],
onSequenceMatch: (match) => {
if (match.type === 'hold') {
switch(match.sequenceId) {
case 'slow-scroll':
setScrollSpeed(1);
break;
case 'medium-scroll':
setScrollSpeed(2);
break;
case 'fast-scroll':
setScrollSpeed(3);
break;
case 'format-selection':
setEditorActions(prev => [...prev, `✨ Text formatted! ${new Date().toLocaleTimeString()}`]);
break;
}
}
}
}
});
// Reset scroll speed on space release
useEffect(() => {
if (keys.lastEvent?.type === 'keyup' && keys.lastEvent.key === Keys.SPACE) {
if (scrollSpeed > 0) {
setEditorActions(prev => [...prev, `📜 Scrolled at speed level ${scrollSpeed}!`]);
setScrollSpeed(0);
}
}
}, [keys.lastEvent, scrollSpeed]);
return (
<div>
<h2>Text Editor Hold Detection</h2>
<div>
<h3>Controls:</h3>
<ul>
<li>Hold SPACE to scroll faster (300ms / 700ms / 1200ms)</li>
<li>Hold SHIFT+F to format selection (800ms)</li>
</ul>
</div>
<div>
<h3>Scroll Speed: {scrollSpeed}/3</h3>
<div style={{width: '200px', height: '20px', backgroundColor: '#ddd', borderRadius: '10px'}}>
<div style={{
width: `${(scrollSpeed / 3) * 100}%`,
height: '100%',
backgroundColor: scrollSpeed === 3 ? '#3b82f6' : scrollSpeed === 2 ? '#10b981' : '#84cc16',
borderRadius: '10px',
transition: 'width 0.3s'
}} />
</div>
<div style={{ marginTop: '5px', fontSize: '14px', color: '#666' }}>
{scrollSpeed === 0 && 'Normal speed'}
{scrollSpeed === 1 && 'Slow scroll active'}
{scrollSpeed === 2 && 'Medium scroll active'}
{scrollSpeed === 3 && 'Fast scroll active'}
</div>
</div>
<div>
<h3>Editor Action Log:</h3>
{editorActions.slice(-5).map((action, i) => (
<p key={i} style={{ fontFamily: 'monospace' }}>📝 {action}</p>
))}
</div>
</div>
);
}
preventDefault API & Browser Shortcut Blocking
tsx
import { useNormalizedKeys, Keys } from 'use-normalized-keys';
import { useState } from 'react';
function PreventDefaultExample() {
const [mode, setMode] = useState<'none' | 'specific' | 'all'>('none');
const keys = useNormalizedKeys({
preventDefault: mode === 'all' ? true :
mode === 'specific' ? [Keys.F5, Keys.F12, Keys.TAB] :
false
});
return (
<div>
<h2>preventDefault API</h2>
<div>
<h3>Prevention Mode:</h3>
<button onClick={() => setMode('none')} style={{backgroundColor: mode === 'none' ? '#4CAF50' : ''}}>
None
</button>
<button onClick={() => setMode('specific')} style={{backgroundColor: mode === 'specific' ? '#4CAF50' : ''}}>
Specific Keys (F5, F12, Tab)
</button>
<button onClick={() => setMode('all')} style={{backgroundColor: mode === 'all' ? '#4CAF50' : ''}}>
All Keys
</button>
</div>
<div>
<h3>Test these shortcuts:</h3>
<ul>
<li>F5 - Refresh page (try with different modes)</li>
<li>F12 - Developer tools</li>
<li>Tab - Tab navigation</li>
<li>Ctrl+S - Save page</li>
</ul>
</div>
<p>Last prevented: {keys.lastEvent?.preventedDefault ? `${keys.lastEvent.key} ✓` : 'None'}</p>
<p>Active keys: {Array.from(keys.pressedKeys).join(', ')}</p>
</div>
);
}
Tap vs Hold Detection
tsx
import { useNormalizedKeys, Keys } from 'use-normalized-keys';
import { useState, useEffect } from 'react';
function TapHoldExample() {
const [threshold, setThreshold] = useState(200);
const [events, setEvents] = useState<string[]>([]);
const keys = useNormalizedKeys({
tapHoldThreshold: threshold,
debug: false
});
// Track tap/hold events
useEffect(() => {
if (keys.lastEvent?.type === 'keyup' && (keys.lastEvent.isTap || keys.lastEvent.isHold)) {
const eventType = keys.lastEvent.isTap ? 'TAP' : 'HOLD';
const newEvent = `${eventType}: ${keys.lastEvent.key} (${keys.lastEvent.duration}ms)`;
setEvents(prev => [...prev.slice(-4), newEvent]); // Keep last 5 events
}
}, [keys.lastEvent]);
return (
<div>
<h2>Tap vs Hold Detection</h2>
<div>
<label>
Threshold: {threshold}ms
<input
type="range"
min="50"
max="1000"
value={threshold}
onChange={(e) => setThreshold(Number(e.target.value))}
/>
</label>
</div>
<div>
<h3>Try tapping or holding keys:</h3>
<p>Press and release keys quickly for taps, hold them for holds</p>
<p>Current: {keys.lastEvent?.key} - {keys.lastEvent?.duration}ms</p>
</div>
<div>
<h3>Recent Events:</h3>
{events.map((event, i) => (
<p key={i} style={{fontFamily: 'monospace'}}>{event}</p>
))}
</div>
</div>
);
}
Cross-Platform Compatibility Demo
tsx
import { useNormalizedKeys, Keys } from 'use-normalized-keys';
function PlatformDemo() {
const keys = useNormalizedKeys({ debug: true });
const platform = navigator.platform;
const isWindows = platform.startsWith('Win');
const isMac = platform.startsWith('Mac');
return (
<div>
<h2>Cross-Platform Compatibility</h2>
<div>
<h3>Detected Platform: {platform}</h3>
<p>Windows: {isWindows ? '✓' : '✗'}</p>
<p>macOS: {isMac ? '✓' : '✗'}</p>
</div>
<div>
<h3>Platform-Specific Features:</h3>
{isWindows && (
<div>
<h4>Windows Features:</h4>
<p>✓ Shift+Numpad phantom event suppression</p>
<p>Try: Hold Shift and press numpad keys (1,2,3)</p>
</div>
)}
{isMac && (
<div>
<h4>macOS Features:</h4>
<p>✓ Meta key timeout handling</p>
<p>Try: Hold Cmd key for extended periods</p>
</div>
)}
<h4>Numpad Detection:</h4>
<p>NumLock: {keys.activeModifiers.numLock ? '🟢 On' : '🔴 Off'}</p>
{keys.lastEvent?.numpadInfo && (
<div>
<p>Last numpad key: {keys.lastEvent.key}</p>
<p>Mode: {keys.lastEvent.numpadInfo.activeMode}</p>
<p>Digit: {keys.lastEvent.numpadInfo.digit}</p>
<p>Navigation: {keys.lastEvent.numpadInfo.navigation}</p>
</div>
)}
</div>
<div>
<h3>Current State:</h3>
<p>Keys: {Array.from(keys.pressedKeys).join(', ')}</p>
<p>Last event: {keys.lastEvent?.key} ({keys.lastEvent?.type})</p>
</div>
</div>
);
}
Complete Text Editor with All Features
tsx
import { useNormalizedKeys, Keys } from 'use-normalized-keys';
import { useState, useEffect, useRef } from 'react';
// Type definitions for better TypeScript support
interface CursorPosition { line: number; column: number; }
interface TextSelection { start: CursorPosition; end: CursorPosition; }
interface EditorAction { type: string; timestamp: number; id: number; }
interface EditorState {
content: string[];
cursor: CursorPosition;
selection: TextSelection | null;
actions: EditorAction[];
mode: 'normal' | 'insert' | 'select';
vimMode: boolean;
}
export default function AdvancedTextEditor() {
const [editorState, setEditorState] = useState<EditorState>({
content: ['Welcome to the advanced text editor!', 'Try keyboard shortcuts:', '- Ctrl+S to save', '- Ctrl+A to select all', '- Hold Space for scroll mode'],
cursor: { line: 0, column: 0 },
selection: null,
actions: [],
mode: 'normal',
vimMode: false
});
const keys = useNormalizedKeys({
sequences: [
{
id: 'vim-escape',
keys: [Keys.j, Keys.k],
type: 'sequence'
},
{
id: 'vim-mode',
keys: [Keys.v, Keys.i, Keys.m],
type: 'sequence'
}
],
onSequenceMatch: (match) => {
if (match.sequenceId === 'vim-escape' && editorState.mode === 'insert') {
setEditorState(prev => ({
...prev,
mode: 'normal'
}));
}
if (match.sequenceId === 'vim-mode') {
setEditorState(prev => ({
...prev,
vimMode: !prev.vimMode,
actions: [...prev.actions, { type: `VIM mode ${!prev.vimMode ? 'enabled' : 'disabled'}`, timestamp: Date.now(), id: Date.now() }]
}));
}
},
preventDefault: [Keys.F5, Keys.TAB], // Block specific browser shortcuts
tapHoldThreshold: 150,
debug: false
});
// Editor update loop using requestAnimationFrame for smooth cursor movement
const animationFrameRef = useRef<number>();
const lastFrameTimeRef = useRef<number>(0);
useEffect(() => {
const editorLoop = (timestamp: number) => {
// Throttle to ~60 FPS
if (timestamp - lastFrameTimeRef.current < 16) {
animationFrameRef.current = requestAnimationFrame(editorLoop);
return;
}
lastFrameTimeRef.current = timestamp;
setEditorState(prev => {
let newState = { ...prev };
const cursor = { ...prev.cursor };
let actions = prev.actions.slice();
// Cursor movement with different speeds
const baseSpeed = 1;
const isAccelerated = keys.isKeyPressed(Keys.SHIFT);
const moveSpeed = isAccelerated ? baseSpeed * 3 : baseSpeed;
// Only process movement if not in insert mode or vim mode allows it
if (prev.mode === 'normal' || !prev.vimMode) {
// Vertical movement
if (keys.isKeyPressed(Keys.ARROW_UP) && cursor.line > 0) {
cursor.line = Math.max(0, cursor.line - moveSpeed);
cursor.column = Math.min(cursor.column, newState.content[cursor.line]?.length || 0);
}
if (keys.isKeyPressed(Keys.ARROW_DOWN) && cursor.line < newState.content.length - 1) {
cursor.line = Math.min(newState.content.length - 1, cursor.line + moveSpeed);
cursor.column = Math.min(cursor.column, newState.content[cursor.line]?.length || 0);
}
// Horizontal movement
if (keys.isKeyPressed(Keys.ARROW_LEFT) && cursor.column > 0) {
cursor.column = Math.max(0, cursor.column - moveSpeed);
}
if (keys.isKeyPressed(Keys.ARROW_RIGHT)) {
const lineLength = newState.content[cursor.line]?.length || 0;
cursor.column = Math.min(lineLength, cursor.column + moveSpeed);
}
}
// Scroll mode with space (tap vs hold for different behaviors)
if (keys.isKeyPressed(Keys.SPACE)) {
const scrollAction = keys.lastEvent?.isHold ? 'Fast scroll' : 'Page scroll';
const scrollSpeed = keys.lastEvent?.isHold ? 3 : 1;
// Add scroll indicator to actions if space just pressed
if (keys.lastEvent?.type === 'keydown' && keys.lastEvent.key === Keys.SPACE) {
actions = actions.concat({
type: `${scrollAction} activated`,
timestamp: Date.now(),
id: Date.now()
});
}
}
return { ...newState, cursor, actions };
});
animationFrameRef.current = requestAnimationFrame(editorLoop);
};
animationFrameRef.current = requestAnimationFrame(editorLoop);
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [keys.isKeyPressed, keys.lastEvent]); // Note: keys.isKeyPressed is stable
// Handle text editing shortcuts
useEffect(() => {
if (keys.lastEvent?.type === 'keydown') {
const isCtrl = keys.activeModifiers.ctrl;
const key = keys.lastEvent.key;
if (isCtrl && key === Keys.s) {
setEditorState(prev => ({
...prev,
actions: [...prev.actions, { type: 'Document saved', timestamp: Date.now(), id: Date.now() }]
}));
}
if (isCtrl && key === Keys.a) {
setEditorState(prev => ({
...prev,
selection: {
start: { line: 0, column: 0 },
end: { line: prev.content.length - 1, column: prev.content[prev.content.length - 1]?.length || 0 }
},
actions: [...prev.actions, { type: 'Select all', timestamp: Date.now(), id: Date.now() }]
}));
}
if (key === Keys.ESCAPE) {
setEditorState(prev => ({
...prev,
mode: prev.vimMode ? 'normal' : prev.mode,
selection: null,
actions: [...prev.actions, { type: 'Escape pressed', timestamp: Date.now(), id: Date.now() }]
}));
}
}
}, [keys.lastEvent, keys.activeModifiers]);
return (
<div>
<h2>Advanced Text Editor</h2>
<div
tabIndex={0}
autoFocus
style={{
position: 'relative',
width: 600,
height: 400,
border: '2px solid #333',
backgroundColor: editorState.vimMode ? '#1a1a1a' : '#ffffff',
color: editorState.vimMode ? '#00ff00' : '#000000',
fontFamily: 'monospace',
fontSize: '14px',
outline: 'none',
overflow: 'hidden'
}}
>
{/* Text content */}
<div style={{ padding: '10px', lineHeight: '1.4' }}>
{editorState.content.map((line, lineIndex) => (
<div key={lineIndex} style={{ position: 'relative', minHeight: '1.4em' }}>
<span style={{ color: '#666', marginRight: '10px', fontSize: '12px' }}>
{(lineIndex + 1).toString().padStart(2, '0')}
</span>
{line}
{/* Cursor */}
{editorState.cursor.line === lineIndex && (
<span
style={{
position: 'absolute',
left: `${80 + editorState.cursor.column * 8.5}px`,
width: '2px',
height: '1.4em',
backgroundColor: editorState.vimMode ? '#00ff00' : '#000000',
animation: 'blink 1s infinite'
}}
/>
)}
</div>
))}
</div>
{/* Selection overlay */}
{editorState.selection && (
<div style={{
position: 'absolute',
top: '10px',
left: '50px',
right: '10px',
backgroundColor: 'rgba(0, 123, 255, 0.2)',
pointerEvents: 'none'
}}>
Selection active
</div>
)}
{/* Status line */}
<div style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '30px',
backgroundColor: editorState.vimMode ? '#333' : '#f0f0f0',
color: editorState.vimMode ? '#00ff00' : '#333',
display: 'flex',
alignItems: 'center',
padding: '0 10px',
fontSize: '12px',
borderTop: '1px solid #ccc'
}}>
<span>Line: {editorState.cursor.line + 1}, Col: {editorState.cursor.column}</span>
<span style={{ marginLeft: '20px' }}>Mode: {editorState.mode.toUpperCase()}</span>
{editorState.vimMode && <span style={{ marginLeft: '20px' }}>VIM MODE</span>}
<span style={{ marginLeft: 'auto' }}>
{keys.isKeyPressed(Keys.SHIFT) && 'SHIFT '}
{keys.isKeyPressed(Keys.SPACE) && (keys.lastEvent?.isHold ? 'FAST SCROLL ' : 'SCROLL ')}
</span>
</div>
</div>
<div style={{ marginTop: '10px' }}>
<p>Mode: {editorState.mode} {editorState.vimMode && '(VIM enabled)'}</p>
<p>Cursor: Line {editorState.cursor.line + 1}, Column {editorState.cursor.column + 1}</p>
<p>Keyboard Shortcuts:</p>
<ul style={{ fontSize: '14px' }}>
<li>Arrow keys: Navigate (Hold Shift for fast movement)</li>
<li>Space: Scroll mode (Hold for fast scroll)</li>
<li>Ctrl+S: Save document</li>
<li>Ctrl+A: Select all</li>
<li>Escape: Exit modes/clear selection</li>
<li>Type "jk" quickly in insert mode to escape (VIM)</li>
<li>Type "vim" to toggle VIM mode</li>
</ul>
<p>Active keys: {Array.from(keys.pressedKeys).join(', ') || 'None'}</p>
<p>Sequences detected: {keys.sequences?.matches.length || 0}</p>
{keys.lastEvent && (
<p style={{ fontFamily: 'monospace', fontSize: '12px' }}>
Last event: {keys.lastEvent.key}
{keys.lastEvent.isTap && ' (tap)'}
{keys.lastEvent.isHold && ' (hold)'}
- {keys.lastEvent.duration}ms
</p>
)}
<div>
<h3>Recent Actions:</h3>
<div style={{ maxHeight: '100px', overflowY: 'auto', fontSize: '12px', fontFamily: 'monospace' }}>
{editorState.actions.slice(-5).map((action, i) => (
<p key={action.id}>📝 {action.type} - {new Date(action.timestamp).toLocaleTimeString()}</p>
))}
</div>
</div>
</div>
<style jsx>{`
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
`}</style>
</div>
);
}