@umituz/react-native-design-system 4.28.4 → 4.28.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/src/atoms/AtomicInput.tsx +2 -2
  3. package/src/index.ts +1 -1
  4. package/src/molecules/Divider/Divider.tsx +2 -3
  5. package/src/molecules/Divider/types.ts +22 -5
  6. package/src/molecules/StepHeader/StepHeader.constants.ts +48 -0
  7. package/src/molecules/StepHeader/StepHeader.tsx +29 -23
  8. package/src/molecules/StepProgress/StepProgress.constants.ts +23 -0
  9. package/src/molecules/StepProgress/StepProgress.tsx +9 -6
  10. package/src/molecules/avatar/Avatar.constants.ts +20 -2
  11. package/src/molecules/avatar/Avatar.tsx +5 -3
  12. package/src/molecules/avatar/Avatar.utils.ts +4 -4
  13. package/src/molecules/avatar/AvatarGroup.tsx +2 -2
  14. package/src/molecules/listitem/styles/listItemStyles.ts +2 -3
  15. package/src/molecules/navigation/hooks/useAppFocusEffect.ts +14 -11
  16. package/src/molecules/navigation/hooks/useAppIsFocused.ts +1 -2
  17. package/src/molecules/navigation/hooks/useAppNavigation.ts +88 -118
  18. package/src/molecules/navigation/hooks/useAppRoute.ts +26 -27
  19. package/src/onboarding/domain/entities/ChatMessage.ts +19 -0
  20. package/src/onboarding/domain/entities/ChatStep.ts +72 -0
  21. package/src/onboarding/index.ts +29 -0
  22. package/src/onboarding/infrastructure/hooks/useChatAnimations.ts +145 -0
  23. package/src/onboarding/presentation/components/chat/ChatMessage.tsx +166 -0
  24. package/src/onboarding/presentation/components/chat/ChatOptionButton.tsx +145 -0
  25. package/src/onboarding/presentation/components/chat/TypingIndicator.tsx +99 -0
  26. package/src/onboarding/presentation/components/chat/index.ts +12 -0
  27. package/src/onboarding/presentation/hooks/useChatOnboarding.ts +278 -0
  28. package/src/onboarding/presentation/screens/ChatOnboardingScreen.tsx +276 -0
  29. package/src/utils/index.ts +13 -0
  30. package/src/utils/responsiveUtils.ts +110 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Chat Message Component
3
+ *
4
+ * Displays a single message in the chat interface
5
+ * Lazy loads animations for optimal bundle size
6
+ * Fully responsive - uses safe area insets and responsive sizing
7
+ */
8
+
9
+ import React, { memo, useEffect, useMemo } from "react";
10
+ import { View, Text, StyleSheet, ViewStyle, Dimensions } from "react-native";
11
+ import { useResponsive } from "../../../../responsive/useResponsive";
12
+
13
+ import type { ChatMessage as ChatMessageEntity } from "../../../domain/entities/ChatMessage";
14
+
15
+ export interface ChatMessageProps {
16
+ /** Message data */
17
+ message: ChatMessageEntity;
18
+
19
+ /** Message background color */
20
+ backgroundColor?: string;
21
+
22
+ /** Message text color */
23
+ textColor?: string;
24
+
25
+ /** Important message highlight color */
26
+ importantColor?: string;
27
+
28
+ /** User message background color */
29
+ userBackgroundColor?: string;
30
+
31
+ /** User message text color */
32
+ userTextColor?: string;
33
+
34
+ /** Additional styles */
35
+ style?: ViewStyle;
36
+
37
+ /** Enable animations (default: true) */
38
+ animate?: boolean;
39
+ }
40
+
41
+ /**
42
+ * Chat message bubble component
43
+ * Responsive design: Uses safe area insets and scales padding/font sizes
44
+ */
45
+ export const ChatMessage = memo(
46
+ ({
47
+ message,
48
+ backgroundColor = "#FFFFFF",
49
+ textColor = "#000000",
50
+ importantColor = "#FF6B6B",
51
+ userBackgroundColor = "#3B82F6",
52
+ userTextColor = "#FFFFFF",
53
+ style,
54
+ animate = true,
55
+ }: ChatMessageProps) => {
56
+ const responsive = useResponsive();
57
+ const [opacity, setOpacity] = React.useState(0);
58
+
59
+ useEffect(() => {
60
+ if (!animate) {
61
+ setOpacity(1);
62
+ return;
63
+ }
64
+
65
+ // Simple fade-in without Reanimated
66
+ const timer = setTimeout(() => {
67
+ setOpacity(1);
68
+ }, 50);
69
+
70
+ return () => clearTimeout(timer);
71
+ }, [animate]);
72
+
73
+ // Responsive padding based on screen size (uses central spacingMultiplier)
74
+ const padding = useMemo(() => Math.floor(12 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
75
+ const fontSize = useMemo(() => Math.floor(16 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
76
+ const lineHeight = useMemo(() => Math.floor(22 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
77
+ const borderRadius = useMemo(() => Math.floor(16 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
78
+ const marginBottom = useMemo(() => Math.floor(8 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
79
+ const borderWidth = useMemo(() => Math.max(2, Math.floor(2 * responsive.spacingMultiplier)), [responsive.spacingMultiplier]);
80
+ const maxTextWidth = useMemo(() => {
81
+ // On tablets, allow wider messages (up to 90% vs 80% on mobile)
82
+ const screenWidth = Dimensions.get('window').width;
83
+ const baseMaxWidth = responsive.insets.left + responsive.insets.right > 20 ? 0.9 : 0.8;
84
+ return Math.floor(screenWidth * baseMaxWidth);
85
+ }, [responsive.insets.left, responsive.insets.right]);
86
+
87
+ const containerStyle = useMemo(
88
+ () => [
89
+ styles.container,
90
+ {
91
+ maxWidth: maxTextWidth,
92
+ padding,
93
+ borderRadius,
94
+ marginBottom,
95
+ },
96
+ message.isUser ? styles.userContainer : styles.botContainer,
97
+ {
98
+ backgroundColor: message.isUser ? userBackgroundColor : backgroundColor,
99
+ opacity,
100
+ },
101
+ message.isImportant && !message.isUser ? {
102
+ borderWidth,
103
+ borderColor: "#FF6B6B",
104
+ } : null,
105
+ style,
106
+ ],
107
+ [
108
+ padding,
109
+ borderRadius,
110
+ marginBottom,
111
+ borderWidth,
112
+ maxTextWidth,
113
+ backgroundColor,
114
+ userBackgroundColor,
115
+ message.isUser,
116
+ message.isImportant,
117
+ opacity,
118
+ style,
119
+ ]
120
+ );
121
+
122
+ const textStyle = useMemo(
123
+ () => [
124
+ styles.text,
125
+ {
126
+ fontSize,
127
+ lineHeight,
128
+ color: message.isUser ? userTextColor : textColor,
129
+ },
130
+ message.isImportant && !message.isUser
131
+ ? { color: importantColor, fontWeight: "600" as const }
132
+ : null,
133
+ ],
134
+ [fontSize, lineHeight, textColor, userTextColor, message.isUser, message.isImportant, importantColor]
135
+ );
136
+
137
+ return (
138
+ <View style={containerStyle}>
139
+ <Text style={textStyle}>{message.text}</Text>
140
+ </View>
141
+ );
142
+ }
143
+ );
144
+
145
+ ChatMessage.displayName = "ChatMessage";
146
+
147
+ const styles = StyleSheet.create({
148
+ container: {
149
+ // maxWidth is set dynamically based on screen size
150
+ padding: 12, // default, will be overridden by responsive value
151
+ borderRadius: 16, // default, will be overridden
152
+ marginBottom: 8, // default, will be overridden
153
+ },
154
+ userContainer: {
155
+ alignSelf: "flex-end",
156
+ borderBottomRightRadius: 4,
157
+ },
158
+ botContainer: {
159
+ alignSelf: "flex-start",
160
+ borderBottomLeftRadius: 4,
161
+ },
162
+ text: {
163
+ fontSize: 16, // default, will be overridden
164
+ lineHeight: 22, // default, will be overridden
165
+ },
166
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Chat Option Button Component
3
+ *
4
+ * Displays a selectable option in the chat interface
5
+ * Lazy loads animations for optimal bundle size
6
+ * Fully responsive - scales padding, font sizes, and icon sizes
7
+ */
8
+
9
+ import React, { memo, useMemo } from "react";
10
+ import { TouchableOpacity, Text, StyleSheet, ViewStyle, View } from "react-native";
11
+ import { useResponsive } from "../../../../responsive/useResponsive";
12
+ import type { ChatOption } from "../../../domain/entities/ChatStep";
13
+
14
+ export interface ChatOptionButtonProps {
15
+ /** Option data */
16
+ option: ChatOption;
17
+
18
+ /** Button press handler */
19
+ onPress: (option: ChatOption) => void;
20
+
21
+ /** Button background color */
22
+ backgroundColor?: string;
23
+
24
+ /** Button text color */
25
+ textColor?: string;
26
+
27
+ /** Button border color */
28
+ borderColor?: string;
29
+
30
+ /** Whether button is disabled */
31
+ disabled?: boolean;
32
+
33
+ /** Additional styles */
34
+ style?: ViewStyle;
35
+
36
+ /** Enable animations (default: true) */
37
+ animate?: boolean;
38
+
39
+ /** Custom icon component */
40
+ renderIcon?: (iconName: string) => React.ReactNode;
41
+ }
42
+
43
+ /**
44
+ * Chat option button component
45
+ * Responsive design: Scales padding, font size, and min-width based on screen size
46
+ */
47
+ export const ChatOptionButton = memo(
48
+ ({
49
+ option,
50
+ onPress,
51
+ backgroundColor = "#F3F4F6",
52
+ textColor = "#1F2937",
53
+ borderColor = "#E5E7EB",
54
+ disabled = false,
55
+ style,
56
+ renderIcon,
57
+ }: ChatOptionButtonProps) => {
58
+ const responsive = useResponsive();
59
+
60
+ const handlePress = () => {
61
+ if (!disabled) {
62
+ onPress(option);
63
+ }
64
+ };
65
+
66
+ // Responsive sizing based on screen width and safe area
67
+ const padding = useMemo(() => Math.floor(14 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
68
+ const borderRadius = useMemo(() => Math.floor(12 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
69
+ const fontSize = useMemo(() => Math.floor(16 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
70
+ const marginBottom = useMemo(() => Math.floor(8 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
71
+ const iconMargin = useMemo(() => Math.floor(12 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
72
+
73
+ // Responsive min-width: tablets allow wider buttons
74
+ const minButtonWidth = useMemo(() => {
75
+ const screenWidth = responsive.insets.left + responsive.insets.right + 375; // Approximate base width
76
+ const baseMinWidth = 200;
77
+ const scaledMinWidth = Math.floor(baseMinWidth * (screenWidth / 375) * responsive.spacingMultiplier);
78
+ return Math.min(scaledMinWidth, baseMinWidth * 1.5); // Cap at 1.5x for very large screens
79
+ }, [responsive.insets.left, responsive.insets.right, responsive.spacingMultiplier]);
80
+
81
+ const buttonStyle = useMemo(
82
+ () => [
83
+ styles.button,
84
+ {
85
+ padding,
86
+ borderRadius,
87
+ marginBottom,
88
+ minWidth: minButtonWidth,
89
+ backgroundColor,
90
+ borderColor,
91
+ opacity: disabled ? 0.5 : 1,
92
+ },
93
+ style,
94
+ ],
95
+ [padding, borderRadius, marginBottom, minButtonWidth, backgroundColor, borderColor, disabled, style]
96
+ );
97
+
98
+ const textStyle = useMemo(
99
+ () => [
100
+ styles.text,
101
+ {
102
+ fontSize,
103
+ color: textColor,
104
+ },
105
+ ],
106
+ [fontSize, textColor]
107
+ );
108
+
109
+ return (
110
+ <TouchableOpacity
111
+ style={buttonStyle}
112
+ onPress={handlePress}
113
+ disabled={disabled}
114
+ activeOpacity={0.7}
115
+ >
116
+ {option.icon && renderIcon && (
117
+ <View style={[styles.iconContainer, { marginRight: iconMargin }]}>{renderIcon(option.icon)}</View>
118
+ )}
119
+ <Text style={textStyle}>{option.label}</Text>
120
+ </TouchableOpacity>
121
+ );
122
+ }
123
+ );
124
+
125
+ ChatOptionButton.displayName = "ChatOptionButton";
126
+
127
+ const styles = StyleSheet.create({
128
+ button: {
129
+ flexDirection: "row",
130
+ alignItems: "center",
131
+ padding: 14, // default, will be overridden by responsive value
132
+ borderRadius: 12, // default, will be overridden
133
+ borderWidth: 1,
134
+ marginBottom: 8, // default, will be overridden
135
+ minWidth: 200, // default, will be overridden
136
+ },
137
+ iconContainer: {
138
+ marginRight: 12, // default, will be overridden
139
+ },
140
+ text: {
141
+ fontSize: 16, // default, will be overridden
142
+ fontWeight: "600",
143
+ flex: 1,
144
+ },
145
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Typing Indicator Component
3
+ *
4
+ * Shows animated typing dots for bot messages
5
+ * Uses CSS animations for minimal bundle impact
6
+ * Fully responsive - scales dot size and spacing on larger screens
7
+ */
8
+
9
+ import React, { memo, useEffect, useState, useMemo } from "react";
10
+ import { View, StyleSheet, ViewStyle } from "react-native";
11
+ import { useResponsive } from "../../../../responsive/useResponsive";
12
+
13
+ export interface TypingIndicatorProps {
14
+ /** Dot color */
15
+ dotColor?: string;
16
+
17
+ /** Container background color */
18
+ backgroundColor?: string;
19
+
20
+ /** Additional styles */
21
+ style?: ViewStyle;
22
+
23
+ /** Animation duration in ms */
24
+ duration?: number;
25
+ }
26
+
27
+ /**
28
+ * Typing indicator dots component
29
+ * Responsive design: Scales dot size and spacing based on screen size
30
+ */
31
+ export const TypingIndicator = memo(
32
+ ({
33
+ dotColor = "#9CA3AF",
34
+ backgroundColor = "#F3F4F6",
35
+ style,
36
+ duration = 800,
37
+ }: TypingIndicatorProps) => {
38
+ const responsive = useResponsive();
39
+ const [phase, setPhase] = useState(0);
40
+
41
+ useEffect(() => {
42
+ const interval = setInterval(() => {
43
+ setPhase((prev) => (prev + 1) % 4);
44
+ }, duration / 4);
45
+
46
+ return () => clearInterval(interval);
47
+ }, [duration]);
48
+
49
+ // Responsive sizing - dots scale slightly on tablets
50
+ const dotSize = useMemo(() => Math.floor(8 * Math.max(1, responsive.spacingMultiplier * 0.8)), [responsive.spacingMultiplier]);
51
+ const dotRadius = useMemo(() => Math.floor(dotSize / 2), [dotSize]);
52
+ const dotSpacing = useMemo(() => Math.floor(3 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
53
+
54
+ const padding = useMemo(() => Math.floor(12 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
55
+ const borderRadius = useMemo(() => Math.floor(16 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
56
+ const marginBottom = useMemo(() => Math.floor(8 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
57
+ const minIndicatorWidth = useMemo(() => Math.floor(60 * responsive.spacingMultiplier), [responsive.spacingMultiplier]);
58
+
59
+ const dotStyle = (index: number) => [
60
+ styles.dot,
61
+ {
62
+ width: dotSize,
63
+ height: dotSize,
64
+ borderRadius: dotRadius,
65
+ marginHorizontal: dotSpacing,
66
+ backgroundColor: dotColor,
67
+ opacity: phase === index ? 1 : 0.3,
68
+ },
69
+ ];
70
+
71
+ return (
72
+ <View style={[styles.container, { backgroundColor, padding, borderRadius, marginBottom, minWidth: minIndicatorWidth }, style]}>
73
+ <View style={dotStyle(0)} />
74
+ <View style={dotStyle(1)} />
75
+ <View style={dotStyle(2)} />
76
+ </View>
77
+ );
78
+ }
79
+ );
80
+
81
+ TypingIndicator.displayName = "TypingIndicator";
82
+
83
+ const styles = StyleSheet.create({
84
+ container: {
85
+ flexDirection: "row",
86
+ alignItems: "center",
87
+ padding: 12, // default, will be overridden
88
+ borderRadius: 16, // default, will be overridden
89
+ alignSelf: "flex-start",
90
+ marginBottom: 8, // default, will be overridden
91
+ minWidth: 60, // default, will be overridden
92
+ },
93
+ dot: {
94
+ width: 8, // default, will be overridden
95
+ height: 8, // default, will be overridden
96
+ borderRadius: 4, // default, will be overridden
97
+ marginHorizontal: 3, // default, will be overridden
98
+ },
99
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Chat Components Exports
3
+ */
4
+
5
+ export { ChatMessage as ChatMessageComponent } from "./ChatMessage";
6
+ export type { ChatMessageProps } from "./ChatMessage";
7
+
8
+ export { ChatOptionButton } from "./ChatOptionButton";
9
+ export type { ChatOptionButtonProps } from "./ChatOptionButton";
10
+
11
+ export { TypingIndicator } from "./TypingIndicator";
12
+ export type { TypingIndicatorProps } from "./TypingIndicator";
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Chat Onboarding Hook
3
+ *
4
+ * Core state management for chat-based onboarding flows
5
+ * Generic and reusable across all apps
6
+ */
7
+
8
+ import { useState, useCallback, useRef, useEffect, useMemo } from "react";
9
+
10
+ import type { ChatStep, ChatOption } from "../../domain/entities/ChatStep";
11
+ import type { ChatMessage } from "../../domain/entities/ChatMessage";
12
+
13
+ export interface UseChatOnboardingOptions {
14
+ /** Chat onboarding flow configuration */
15
+ flow: Record<string, ChatStep>;
16
+
17
+ /** Initial step ID (default: first step in flow) */
18
+ initialStepId?: string;
19
+
20
+ /** Callback when onboarding completes */
21
+ onComplete?: () => void;
22
+
23
+ /** Callback when user skips onboarding */
24
+ onSkip?: () => void;
25
+
26
+ /** Delay between messages (ms) */
27
+ messageDelay?: number;
28
+
29
+ /** Store progress persistently */
30
+ storageKey?: string;
31
+ }
32
+
33
+ export interface UseChatOnboardingReturn {
34
+ /** Current chat step */
35
+ currentStep: ChatStep | null;
36
+
37
+ /** Current step ID */
38
+ currentStepId: string;
39
+
40
+ /** Array of chat messages */
41
+ messages: ChatMessage[];
42
+
43
+ /** Whether to show options */
44
+ showOptions: boolean;
45
+
46
+ /** Whether processing user input */
47
+ isProcessing: boolean;
48
+
49
+ /** Whether to show typing indicator */
50
+ showTypingIndicator: boolean;
51
+
52
+ /** Handle option selection */
53
+ handleOptionSelect: (option: ChatOption) => void;
54
+
55
+ /** Reset onboarding flow */
56
+ handleReset: () => void;
57
+
58
+ /** Navigate to specific step */
59
+ setCurrentStepId: (stepId: string) => void;
60
+
61
+ /** Submit name input */
62
+ handleSubmitName: (name: string) => void;
63
+
64
+ /** Skip current step */
65
+ handleSkip: () => void;
66
+ }
67
+
68
+ /**
69
+ * Hook for managing chat onboarding state
70
+ */
71
+ export const useChatOnboarding = ({
72
+ flow,
73
+ initialStepId,
74
+ onComplete,
75
+ onSkip,
76
+ messageDelay = 500,
77
+ }: UseChatOnboardingOptions): UseChatOnboardingReturn => {
78
+ // Get initial step ID
79
+ const getInitialStepId = useCallback(() => {
80
+ if (initialStepId) return initialStepId;
81
+ const stepIds = Object.keys(flow);
82
+ return stepIds.length > 0 ? stepIds[0] : "";
83
+ }, [flow, initialStepId]);
84
+
85
+ // State
86
+ const [currentStepId, setCurrentStepId] = useState<string>(getInitialStepId);
87
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
88
+ const [showOptions, setShowOptions] = useState<boolean>(false);
89
+ const [isProcessing, setIsProcessing] = useState<boolean>(false);
90
+ const [showTypingIndicator, setShowTypingIndicator] = useState<boolean>(false);
91
+
92
+ // Refs
93
+ const messageTimersRef = useRef<NodeJS.Timeout[]>([]);
94
+ const autoAdvanceTimerRef = useRef<NodeJS.Timeout | null>(null);
95
+
96
+ // Memoize current step
97
+ const currentStep = useMemo(() => {
98
+ return flow[currentStepId] || null;
99
+ }, [flow, currentStepId]);
100
+
101
+ // Clear all timers
102
+ const clearTimers = useCallback(() => {
103
+ messageTimersRef.current.forEach((timer) => {
104
+ clearTimeout(timer);
105
+ });
106
+ messageTimersRef.current = [];
107
+
108
+ if (autoAdvanceTimerRef.current) {
109
+ clearTimeout(autoAdvanceTimerRef.current);
110
+ autoAdvanceTimerRef.current = null;
111
+ }
112
+ }, []);
113
+
114
+ // Load messages for current step
115
+ useEffect(() => {
116
+ if (!currentStep) return;
117
+
118
+ clearTimers();
119
+ setMessages([]);
120
+ setShowOptions(false);
121
+ setShowTypingIndicator(true);
122
+
123
+ const timers: NodeJS.Timeout[] = [];
124
+
125
+ // Show typing indicator first
126
+ const hideTypingDelay = currentStep.messages.length > 0 ? 300 : 0;
127
+ timers.push(
128
+ setTimeout(() => {
129
+ setShowTypingIndicator(false);
130
+ }, hideTypingDelay)
131
+ );
132
+
133
+ // Add messages with staggered delays
134
+ let accumulatedDelay = hideTypingDelay;
135
+ currentStep.messages.forEach((msg, index) => {
136
+ const delay = accumulatedDelay + (index > 0 ? messageDelay : 0);
137
+ timers.push(
138
+ setTimeout(() => {
139
+ setMessages((prev) => [
140
+ ...prev,
141
+ {
142
+ text: msg,
143
+ isUser: false,
144
+ },
145
+ ]);
146
+ }, delay)
147
+ );
148
+ accumulatedDelay = delay + msg.length * 10; // Dynamic delay based on message length
149
+ });
150
+
151
+ // Auto-advance if configured
152
+ if (currentStep.autoNext) {
153
+ const autoAdvanceDelay = accumulatedDelay + (currentStep.delay || 2000);
154
+ const timer = setTimeout(() => {
155
+ setCurrentStepId(currentStep.autoNext!);
156
+ }, autoAdvanceDelay);
157
+ autoAdvanceTimerRef.current = timer;
158
+ } else {
159
+ // Show options after messages
160
+ const optionsDelay = accumulatedDelay + 500;
161
+ timers.push(
162
+ setTimeout(() => {
163
+ if (currentStep.options && currentStep.options.length > 0) {
164
+ setShowOptions(true);
165
+ }
166
+ }, optionsDelay)
167
+ );
168
+ }
169
+
170
+ messageTimersRef.current = timers;
171
+
172
+ return () => {
173
+ clearTimers();
174
+ };
175
+ }, [currentStep, messageDelay, clearTimers]);
176
+
177
+ // Handle option selection
178
+ const handleOptionSelect = useCallback(
179
+ (option: ChatOption) => {
180
+ if (isProcessing) return;
181
+
182
+ setIsProcessing(true);
183
+ setShowOptions(false);
184
+
185
+ // Add user message
186
+ setMessages((prev) => [
187
+ ...prev,
188
+ {
189
+ text: option.label,
190
+ isUser: true,
191
+ },
192
+ ]);
193
+
194
+ // Navigate to next step
195
+ setTimeout(() => {
196
+ setCurrentStepId(option.next);
197
+ setIsProcessing(false);
198
+ }, 600);
199
+ },
200
+ [isProcessing]
201
+ );
202
+
203
+ // Handle name submission
204
+ const handleSubmitName = useCallback(
205
+ (name: string) => {
206
+ if (!currentStep || isProcessing) return;
207
+
208
+ setIsProcessing(true);
209
+
210
+ // Skip if empty and allowed
211
+ if (!name.trim() && currentStep.skipIfEmpty) {
212
+ setCurrentStepId(currentStep.next || "");
213
+ setIsProcessing(false);
214
+ return;
215
+ }
216
+
217
+ // Add user message
218
+ setMessages((prev) => [
219
+ ...prev,
220
+ {
221
+ text: name.trim(),
222
+ isUser: true,
223
+ },
224
+ ]);
225
+
226
+ // Navigate to next step
227
+ setTimeout(() => {
228
+ if (currentStep?.next) {
229
+ setCurrentStepId(currentStep.next);
230
+ }
231
+ setIsProcessing(false);
232
+ }, 600);
233
+ },
234
+ [currentStep, isProcessing]
235
+ );
236
+
237
+ // Handle skip
238
+ const handleSkip = useCallback(() => {
239
+ clearTimers();
240
+ onSkip?.();
241
+ }, [clearTimers, onSkip]);
242
+
243
+ // Handle reset
244
+ const handleReset = useCallback(() => {
245
+ clearTimers();
246
+ setCurrentStepId(getInitialStepId());
247
+ setMessages([]);
248
+ setShowOptions(false);
249
+ setIsProcessing(false);
250
+ setShowTypingIndicator(false);
251
+ }, [clearTimers, getInitialStepId]);
252
+
253
+ // Handle completion
254
+ useEffect(() => {
255
+ if (currentStep?.isComplete) {
256
+ const timer = setTimeout(() => {
257
+ onComplete?.();
258
+ }, currentStep.delay || 2000);
259
+
260
+ return () => clearTimeout(timer);
261
+ }
262
+ return undefined;
263
+ }, [currentStep, onComplete]);
264
+
265
+ return {
266
+ currentStep,
267
+ currentStepId,
268
+ messages,
269
+ showOptions,
270
+ isProcessing,
271
+ showTypingIndicator,
272
+ handleOptionSelect,
273
+ handleReset,
274
+ setCurrentStepId,
275
+ handleSubmitName,
276
+ handleSkip,
277
+ };
278
+ };