Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions docs/demo/UseTypingEffectDemo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, { useState } from 'react';
import { useTypingEffect } from 'react-haiku';

export const UseTypingEffectDemo = () => {
const [customText, setCustomText] = useState('Hello, welcome to the typing effect demo!');
const [isCompleted, setIsCompleted] = useState(false);

const {
displayedText,
isTyping,
startTyping,
reset
} = useTypingEffect(customText, {
baseDelay: 50,
randomDelay: 100,
onComplete: () => setIsCompleted(true)
});

const handleStartTyping = () => {
setIsCompleted(false);
startTyping();
};

const handleReset = () => {
setIsCompleted(false);
reset();
};

const handleTextChange = (e) => {
setCustomText(e.target.value || 'Hello, welcome to the typing effect demo!');
setIsCompleted(false);
};

return (
<div style={{
padding: '20px',
border: '1px solid #ddd',
borderRadius: '8px',
fontFamily: 'monospace'
}}>
<h3>useTypingEffect Demo</h3>

<div style={{ marginBottom: '20px' }}>
<label htmlFor="text-input" style={{ display: 'block', marginBottom: '8px' }}>
Text to animate:
</label>
<input
id="text-input"
type="text"
value={customText}
onChange={handleTextChange}
style={{
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #ccc',
fontFamily: 'monospace'
}}
placeholder="Enter text to animate..."
/>
</div>

<div style={{
minHeight: '60px',
padding: '15px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
marginBottom: '20px',
fontSize: '18px',
lineHeight: '1.4'
}}>
<span>{displayedText}</span>
{isTyping && (
<span style={{
animation: 'blink 1s infinite',
marginLeft: '2px'
}}>|</span>
)}
</div>

<div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
<button
onClick={handleStartTyping}
disabled={isTyping}
style={{
padding: '10px 20px',
backgroundColor: isTyping ? '#ccc' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isTyping ? 'not-allowed' : 'pointer',
fontSize: '14px'
}}
>
{isTyping ? 'Typing...' : 'Start Typing'}
</button>

<button
onClick={handleReset}
style={{
padding: '10px 20px',
backgroundColor: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Reset
</button>
</div>

<div style={{ fontSize: '14px', color: '#666' }}>
<p>
<strong>Status:</strong> {
isTyping ? '🔄 Typing in progress...' :
isCompleted ? '✅ Typing completed!' :
'⏸️ Ready to start'
}
</p>
<p>
<strong>Characters:</strong> {displayedText.length} / {customText.length}
</p>
</div>

<style jsx>{`
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
`}</style>
</div>
);
};
124 changes: 124 additions & 0 deletions docs/docs/hooks/useTypingEffect.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# useTypingEffect()

The `useTypingEffect()` hook simulates a typing effect by progressively updating the state with randomized delays for a natural feel.

### Import

```jsx
import { useTypingEffect } from 'react-haiku';
```

### Usage

import { UseTypingEffectDemo } from '../../demo/UseTypingEffectDemo.jsx';

<UseTypingEffectDemo />

```jsx
import { useTypingEffect } from 'react-haiku';

export const Component = () => {
const { displayedText, isTyping, startTyping, reset } = useTypingEffect(
'Hello, welcome to my website!',
{
baseDelay: 50,
randomDelay: 100,
onComplete: () => console.log('Typing completed!')
}
);

return (
<div>
<p style={{ fontFamily: 'monospace', fontSize: '18px' }}>
{displayedText}
{isTyping && <span>|</span>}
</p>
<button onClick={startTyping} disabled={isTyping}>
{isTyping ? 'Typing...' : 'Start Typing'}
</button>
<button onClick={reset}>Reset</button>
</div>
);
};
```

### Examples

#### Basic Usage

```jsx
const { displayedText, startTyping } = useTypingEffect('Hello World!');
```

#### With Custom Timing

```jsx
const { displayedText, isTyping, startTyping } = useTypingEffect(
'This types slower with more variation...',
{
baseDelay: 100, // Slower base speed
randomDelay: 200 // More random variation
}
);
```

#### With Completion Callback

```jsx
const [isComplete, setIsComplete] = useState(false);

const { displayedText, startTyping } = useTypingEffect(
'Thanks for reading!',
{
onComplete: () => setIsComplete(true)
}
);
```

#### Multiple Typing Effects

```jsx
const intro = useTypingEffect('Hello, I am');
const name = useTypingEffect('John Doe', { baseDelay: 80 });

const handleStart = () => {
intro.startTyping();
// Start second effect after first completes
setTimeout(() => name.startTyping(), 2000);
};
```

### Features

- ✅ **Natural Typing Feel**: Uses randomized delays between characters
- ✅ **Configurable Speed**: Customize base delay and random variation
- ✅ **Reset Support**: Multiple calls to `startTyping()` reset and restart
- ✅ **Completion Callback**: Know when typing animation finishes
- ✅ **TypeScript Support**: Fully typed with comprehensive interfaces
- ✅ **Cleanup**: Automatic timer cleanup on unmount

### Notes

- The `text` parameter must be a non-empty string, otherwise the hook will throw an error
- Calling `startTyping()` multiple times will reset the current animation and start fresh
- The hook automatically cleans up any pending timers when the component unmounts
- Random delays create a more natural typing rhythm compared to fixed intervals
- The `isTyping` state can be used to show/hide typing indicators or disable controls

### Parameters

- **`text`** (string, required) - The text to animate. Must be a non-empty string.
- **`options`** (UseTypingEffectOptions, optional) - Configuration options for the typing effect.

### Options

- **`baseDelay`** (number, default: 50) - Base delay between characters in milliseconds.
- **`randomDelay`** (number, default: 100) - Maximum additional random delay in milliseconds for natural variation.
- **`onComplete`** (function, optional) - Callback function called when typing animation completes.

### Return Value

- **`displayedText`** (string) - The currently displayed portion of the text.
- **`isTyping`** (boolean) - Whether the typing animation is currently running.
- **`startTyping`** (function) - Function to start the typing animation. Resets state if called multiple times.
- **`reset`** (function) - Function to reset the displayed text to empty and stop any active animation.
104 changes: 104 additions & 0 deletions lib/hooks/useTypingEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useState, useCallback, useRef, useEffect } from 'react';

interface UseTypingEffectOptions {
/** Base delay between characters in milliseconds */
baseDelay?: number;
/** Maximum additional random delay in milliseconds */
randomDelay?: number;
/** Function called when typing animation completes */
onComplete?: () => void;
}

interface UseTypingEffectReturn {
/** Current displayed text */
displayedText: string;
/** Whether the typing animation is currently running */
isTyping: boolean;
/** Function to start the typing animation */
startTyping: () => void;
/** Function to reset the displayed text */
reset: () => void;
}

/**
* Hook that simulates a typing effect by progressively updating the state
* with randomized delays for a natural feel.
*
* @param text - The text to animate (must be non-empty string)
* @param options - Configuration options for the typing effect
* @returns Object containing displayed text, typing state, and control functions
*/
export function useTypingEffect(
text: string,
options: UseTypingEffectOptions = {}
): UseTypingEffectReturn {
const {
baseDelay = 50,
randomDelay = 100,
onComplete
} = options;

// Validate text parameter
if (typeof text !== 'string' || text.length === 0) {
throw new Error('useTypingEffect: text parameter must be a non-empty string');
}

const [displayedText, setDisplayedText] = useState('');
const [isTyping, setIsTyping] = useState(false);
const timeoutRef = useRef<number | null>(null);
const currentIndexRef = useRef(0);

// Clear any existing timeout when component unmounts
useEffect(() => {
return () => {
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
}
};
}, []);

const reset = useCallback(() => {
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
setDisplayedText('');
setIsTyping(false);
currentIndexRef.current = 0;
}, []);

const typeNextCharacter = useCallback(() => {
if (currentIndexRef.current < text.length) {
setDisplayedText(text.slice(0, currentIndexRef.current + 1));
currentIndexRef.current += 1;

// Calculate randomized delay for natural typing feel
const delay = baseDelay + Math.random() * randomDelay;

timeoutRef.current = window.setTimeout(typeNextCharacter, delay);
} else {
// Typing complete
setIsTyping(false);
onComplete?.();
}
}, [text, baseDelay, randomDelay, onComplete]);

const startTyping = useCallback(() => {
// Reset state before starting (handles multiple calls)
reset();

setIsTyping(true);
currentIndexRef.current = 0;

// Start typing with initial delay
const initialDelay = baseDelay + Math.random() * randomDelay;
timeoutRef.current = window.setTimeout(typeNextCharacter, initialDelay);
}, [reset, baseDelay, randomDelay, typeNextCharacter]);

return {
displayedText,
isTyping,
startTyping,
reset
};
}
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export { usePermission, UsePermissionState } from './hooks/usePermission';
export { useTimer } from './hooks/useTimer';
export { useWebSocket } from './hooks/useWebSocket';
export { useGeolocation } from './hooks/useGeolocation';
export { useTypingEffect } from './hooks/useTypingEffect';

export { If } from './utils/If';
export { Show } from './utils/Show';
Expand Down
Loading