@vvedantb/ui 0.1.0 → 0.1.2

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 (74) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +3 -2
  3. package/src/ai-elements/artifact.tsx +99 -99
  4. package/src/ai-elements/attachments.tsx +366 -366
  5. package/src/ai-elements/chain-of-thought.tsx +212 -212
  6. package/src/ai-elements/code-block.tsx +478 -478
  7. package/src/ai-elements/conversation.tsx +157 -157
  8. package/src/ai-elements/image.tsx +25 -25
  9. package/src/ai-elements/inline-citation.tsx +252 -252
  10. package/src/ai-elements/message.tsx +269 -269
  11. package/src/ai-elements/model-selector.tsx +211 -211
  12. package/src/ai-elements/prompt-input.tsx +1018 -1018
  13. package/src/ai-elements/queue.tsx +251 -251
  14. package/src/ai-elements/reasoning.tsx +178 -178
  15. package/src/ai-elements/shimmer.tsx +56 -56
  16. package/src/ai-elements/sources.tsx +77 -77
  17. package/src/ai-elements/speech-input.tsx +290 -290
  18. package/src/ai-elements/suggestion.tsx +191 -191
  19. package/src/css.d.ts +2 -2
  20. package/src/index.ts +96 -96
  21. package/src/motion/presets.ts +35 -35
  22. package/src/motion/presets.vibot.ts +105 -105
  23. package/src/ui/_menu-classes.ts +30 -30
  24. package/src/ui/_menu-classes.vibot.ts +33 -33
  25. package/src/ui/accordion.tsx +81 -81
  26. package/src/ui/accordion.verve.tsx +56 -56
  27. package/src/ui/badge.tsx +34 -34
  28. package/src/ui/breadcrumb.tsx +83 -83
  29. package/src/ui/button-group.tsx +84 -84
  30. package/src/ui/button.tsx +57 -57
  31. package/src/ui/card.tsx +27 -27
  32. package/src/ui/carousel.tsx +211 -211
  33. package/src/ui/checkbox.tsx +27 -27
  34. package/src/ui/clear-input.tsx +170 -170
  35. package/src/ui/clear-input.vibot.tsx +76 -76
  36. package/src/ui/clearInputDissolve.ts +214 -214
  37. package/src/ui/collapsible.tsx +7 -7
  38. package/src/ui/command.tsx +148 -148
  39. package/src/ui/context-menu.tsx +173 -173
  40. package/src/ui/dialog.tsx +149 -149
  41. package/src/ui/dropdown-menu.tsx +178 -178
  42. package/src/ui/hover-card.tsx +31 -31
  43. package/src/ui/input-group.tsx +162 -162
  44. package/src/ui/input-group.verve.tsx +150 -150
  45. package/src/ui/input.tsx +21 -21
  46. package/src/ui/kbd.tsx +18 -18
  47. package/src/ui/label.tsx +20 -20
  48. package/src/ui/labeled-switch-row.tsx +53 -53
  49. package/src/ui/modalTransition.ts +85 -85
  50. package/src/ui/pagination.tsx +47 -47
  51. package/src/ui/popover.tsx +33 -33
  52. package/src/ui/progress.tsx +25 -25
  53. package/src/ui/radix-select.tsx +202 -202
  54. package/src/ui/scroll-area.tsx +55 -55
  55. package/src/ui/scroll-area.verve.tsx +43 -43
  56. package/src/ui/search-input.tsx +29 -29
  57. package/src/ui/select.tsx +163 -163
  58. package/src/ui/separator.tsx +28 -28
  59. package/src/ui/sheet.tsx +87 -87
  60. package/src/ui/skeleton.tsx +18 -18
  61. package/src/ui/sonner.css +16 -16
  62. package/src/ui/sonner.tsx +47 -47
  63. package/src/ui/spinner.tsx +27 -27
  64. package/src/ui/surface-classes.ts +55 -55
  65. package/src/ui/switch.tsx +26 -26
  66. package/src/ui/table.tsx +123 -123
  67. package/src/ui/tabs.tsx +140 -140
  68. package/src/ui/tabsSliding.ts +56 -56
  69. package/src/ui/textarea.tsx +21 -21
  70. package/src/ui/time-picker.tsx +170 -170
  71. package/src/ui/tooltip.tsx +25 -25
  72. package/src/ui/visually-hidden.tsx +3 -3
  73. package/src/utils/cn.ts +16 -16
  74. package/src/utils/ref.ts +10 -10
@@ -1,290 +1,290 @@
1
- "use client";
2
- import type { ComponentProps } from "react";
3
- import { IconMicrophone, IconSquare } from "@tabler/icons-react";
4
- import { Button } from "../ui/button";
5
- import { Spinner } from "../ui/spinner";
6
- import { cn } from "../utils/cn";
7
- import { useEffect, useRef, useState } from "react";
8
- interface SpeechRecognition extends EventTarget {
9
- continuous: boolean;
10
- interimResults: boolean;
11
- lang: string;
12
- start(): void;
13
- stop(): void;
14
- onstart: ((this: SpeechRecognition, ev: Event) => void) | null;
15
- onend: ((this: SpeechRecognition, ev: Event) => void) | null;
16
- onresult:
17
- | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void)
18
- | null;
19
- onerror:
20
- | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void)
21
- | null;
22
- }
23
- interface SpeechRecognitionEvent extends Event {
24
- results: SpeechRecognitionResultList;
25
- resultIndex: number;
26
- }
27
- interface SpeechRecognitionResultList {
28
- readonly length: number;
29
- item(index: number): SpeechRecognitionResult;
30
- [index: number]: SpeechRecognitionResult;
31
- }
32
- interface SpeechRecognitionResult {
33
- readonly length: number;
34
- item(index: number): SpeechRecognitionAlternative;
35
- [index: number]: SpeechRecognitionAlternative;
36
- isFinal: boolean;
37
- }
38
- interface SpeechRecognitionAlternative {
39
- transcript: string;
40
- confidence: number;
41
- }
42
- interface SpeechRecognitionErrorEvent extends Event {
43
- error: string;
44
- }
45
- declare global {
46
- interface Window {
47
- SpeechRecognition: new () => SpeechRecognition;
48
- webkitSpeechRecognition: new () => SpeechRecognition;
49
- }
50
- }
51
- type SpeechInputMode = "speech-recognition" | "media-recorder" | "none";
52
- export type SpeechInputProps = ComponentProps<typeof Button> & {
53
- onTranscriptionChange?: (text: string) => void;
54
- onAudioRecorded?: (audioBlob: Blob) => Promise<string>;
55
- lang?: string;
56
- };
57
- const detectSpeechInputMode = (): SpeechInputMode => {
58
- if (typeof window === "undefined") {
59
- return "none";
60
- }
61
- if ("SpeechRecognition" in window || "webkitSpeechRecognition" in window) {
62
- return "speech-recognition";
63
- }
64
- if ("MediaRecorder" in window && "mediaDevices" in navigator) {
65
- return "media-recorder";
66
- }
67
- return "none";
68
- };
69
- export const SpeechInput = ({
70
- className,
71
- onTranscriptionChange,
72
- onAudioRecorded,
73
- lang = "en-US",
74
- ...props
75
- }: SpeechInputProps) => {
76
- const [isListening, setIsListening] = useState(false);
77
- const [isProcessing, setIsProcessing] = useState(false);
78
- const [mode] = useState<SpeechInputMode>(detectSpeechInputMode);
79
- const [isRecognitionReady, setIsRecognitionReady] = useState(false);
80
- const recognitionRef = useRef<SpeechRecognition | null>(null);
81
- const mediaRecorderRef = useRef<MediaRecorder | null>(null);
82
- const streamRef = useRef<MediaStream | null>(null);
83
- const audioChunksRef = useRef<Blob[]>([]);
84
- const onTranscriptionChangeRef = useRef<
85
- SpeechInputProps["onTranscriptionChange"]
86
- >(onTranscriptionChange);
87
- const onAudioRecordedRef =
88
- useRef<SpeechInputProps["onAudioRecorded"]>(onAudioRecorded);
89
- onTranscriptionChangeRef.current = onTranscriptionChange;
90
- onAudioRecordedRef.current = onAudioRecorded;
91
- useEffect(() => {
92
- if (mode !== "speech-recognition") {
93
- return;
94
- }
95
- const SpeechRecognitionCtor =
96
- window.SpeechRecognition || window.webkitSpeechRecognition;
97
- const speechRecognition = new SpeechRecognitionCtor();
98
- speechRecognition.continuous = true;
99
- speechRecognition.interimResults = true;
100
- speechRecognition.lang = lang;
101
- const handleStart = () => {
102
- setIsListening(true);
103
- };
104
- const handleEnd = () => {
105
- setIsListening(false);
106
- };
107
- const handleResult = (event: Event) => {
108
- const speechEvent = event as SpeechRecognitionEvent;
109
- let finalTranscript = "";
110
- for (
111
- let i = speechEvent.resultIndex;
112
- i < speechEvent.results.length;
113
- i += 1
114
- ) {
115
- const result = speechEvent.results[i];
116
- if (result?.isFinal) {
117
- finalTranscript += result[0]?.transcript ?? "";
118
- }
119
- }
120
- if (finalTranscript) {
121
- onTranscriptionChangeRef.current?.(finalTranscript);
122
- }
123
- };
124
- const handleError = () => {
125
- setIsListening(false);
126
- };
127
- speechRecognition.addEventListener("start", handleStart);
128
- speechRecognition.addEventListener("end", handleEnd);
129
- speechRecognition.addEventListener("result", handleResult);
130
- speechRecognition.addEventListener("error", handleError);
131
- recognitionRef.current = speechRecognition;
132
- setIsRecognitionReady(true);
133
- return () => {
134
- speechRecognition.removeEventListener("start", handleStart);
135
- speechRecognition.removeEventListener("end", handleEnd);
136
- speechRecognition.removeEventListener("result", handleResult);
137
- speechRecognition.removeEventListener("error", handleError);
138
- speechRecognition.stop();
139
- recognitionRef.current = null;
140
- setIsRecognitionReady(false);
141
- };
142
- }, [mode, lang]);
143
- useEffect(
144
- () => () => {
145
- if (mediaRecorderRef.current?.state === "recording") {
146
- mediaRecorderRef.current.stop();
147
- }
148
- if (streamRef.current) {
149
- for (const track of streamRef.current.getTracks()) {
150
- track.stop();
151
- }
152
- }
153
- },
154
- [],
155
- );
156
- const startMediaRecorder = async () => {
157
- if (!onAudioRecordedRef.current) {
158
- return;
159
- }
160
- try {
161
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
162
- streamRef.current = stream;
163
- const mediaRecorder = new MediaRecorder(stream);
164
- audioChunksRef.current = [];
165
- const handleDataAvailable = (event: BlobEvent) => {
166
- if (event.data.size > 0) {
167
- audioChunksRef.current.push(event.data);
168
- }
169
- };
170
- const handleStop = async () => {
171
- for (const track of stream.getTracks()) {
172
- track.stop();
173
- }
174
- streamRef.current = null;
175
- const audioBlob = new Blob(audioChunksRef.current, {
176
- type: "audio/webm",
177
- });
178
- if (audioBlob.size > 0 && onAudioRecordedRef.current) {
179
- setIsProcessing(true);
180
- try {
181
- const transcript = await onAudioRecordedRef.current(audioBlob);
182
- if (transcript) {
183
- onTranscriptionChangeRef.current?.(transcript);
184
- }
185
- } catch {
186
- } finally {
187
- setIsProcessing(false);
188
- }
189
- }
190
- };
191
- const handleError = () => {
192
- setIsListening(false);
193
- for (const track of stream.getTracks()) {
194
- track.stop();
195
- }
196
- streamRef.current = null;
197
- };
198
- mediaRecorder.addEventListener("dataavailable", handleDataAvailable);
199
- mediaRecorder.addEventListener("stop", handleStop);
200
- mediaRecorder.addEventListener("error", handleError);
201
- mediaRecorderRef.current = mediaRecorder;
202
- mediaRecorder.start();
203
- setIsListening(true);
204
- } catch {
205
- setIsListening(false);
206
- }
207
- };
208
- const stopMediaRecorder = () => {
209
- if (mediaRecorderRef.current?.state === "recording") {
210
- mediaRecorderRef.current.stop();
211
- }
212
- setIsListening(false);
213
- };
214
- const toggleListening = () => {
215
- if (mode === "speech-recognition" && recognitionRef.current) {
216
- if (isListening) {
217
- recognitionRef.current.stop();
218
- } else {
219
- recognitionRef.current.start();
220
- }
221
- } else if (mode === "media-recorder") {
222
- if (isListening) {
223
- stopMediaRecorder();
224
- } else {
225
- startMediaRecorder();
226
- }
227
- }
228
- };
229
- const isDisabled =
230
- mode === "none" ||
231
- (mode === "speech-recognition" && !isRecognitionReady) ||
232
- (mode === "media-recorder" && !onAudioRecorded) ||
233
- isProcessing;
234
- return (
235
- <div className="relative inline-flex items-center justify-center">
236
- {isListening
237
- ? [0, 1, 2].map((index) => (
238
- <div
239
- className="absolute inset-0 animate-ping rounded-full border-2 border-danger/30"
240
- key={index}
241
- style={{
242
- animationDelay: `${index * 0.3}s`,
243
- animationDuration: "2s",
244
- }}
245
- />
246
- ))
247
- : null}
248
-
249
- <Button
250
- className={cn(
251
- "relative z-10 rounded-full transition-[background-color,transform] duration-200 ease-smooth active:duration-100 active:ease-out",
252
- isListening
253
- ? "bg-destructive text-danger-foreground hover:bg-destructive/80 hover:text-danger-foreground"
254
- : "bg-primary text-primary-foreground hover:bg-primary/80 hover:text-primary-foreground",
255
- className,
256
- )}
257
- disabled={isDisabled}
258
- onClick={toggleListening}
259
- {...props}
260
- >
261
- <span className="relative inline-flex size-4 items-center justify-center">
262
- <span
263
- className={cn(
264
- "absolute inset-0 flex items-center justify-center transition-opacity duration-150 ease-out",
265
- isProcessing ? "opacity-100" : "opacity-0",
266
- )}
267
- >
268
- <Spinner />
269
- </span>
270
- <span
271
- className={cn(
272
- "absolute inset-0 flex items-center justify-center transition-opacity duration-150 ease-out",
273
- !isProcessing && isListening ? "opacity-100" : "opacity-0",
274
- )}
275
- >
276
- <IconSquare className="size-4" />
277
- </span>
278
- <span
279
- className={cn(
280
- "flex items-center justify-center transition-opacity duration-150 ease-out",
281
- isProcessing || isListening ? "opacity-0" : "opacity-100",
282
- )}
283
- >
284
- <IconMicrophone className="size-4" />
285
- </span>
286
- </span>
287
- </Button>
288
- </div>
289
- );
290
- };
1
+ "use client";
2
+ import type { ComponentProps } from "react";
3
+ import { IconMicrophone, IconSquare } from "@tabler/icons-react";
4
+ import { Button } from "../ui/button";
5
+ import { Spinner } from "../ui/spinner";
6
+ import { cn } from "../utils/cn";
7
+ import { useEffect, useRef, useState } from "react";
8
+ interface SpeechRecognition extends EventTarget {
9
+ continuous: boolean;
10
+ interimResults: boolean;
11
+ lang: string;
12
+ start(): void;
13
+ stop(): void;
14
+ onstart: ((this: SpeechRecognition, ev: Event) => void) | null;
15
+ onend: ((this: SpeechRecognition, ev: Event) => void) | null;
16
+ onresult:
17
+ | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void)
18
+ | null;
19
+ onerror:
20
+ | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void)
21
+ | null;
22
+ }
23
+ interface SpeechRecognitionEvent extends Event {
24
+ results: SpeechRecognitionResultList;
25
+ resultIndex: number;
26
+ }
27
+ interface SpeechRecognitionResultList {
28
+ readonly length: number;
29
+ item(index: number): SpeechRecognitionResult;
30
+ [index: number]: SpeechRecognitionResult;
31
+ }
32
+ interface SpeechRecognitionResult {
33
+ readonly length: number;
34
+ item(index: number): SpeechRecognitionAlternative;
35
+ [index: number]: SpeechRecognitionAlternative;
36
+ isFinal: boolean;
37
+ }
38
+ interface SpeechRecognitionAlternative {
39
+ transcript: string;
40
+ confidence: number;
41
+ }
42
+ interface SpeechRecognitionErrorEvent extends Event {
43
+ error: string;
44
+ }
45
+ declare global {
46
+ interface Window {
47
+ SpeechRecognition: new () => SpeechRecognition;
48
+ webkitSpeechRecognition: new () => SpeechRecognition;
49
+ }
50
+ }
51
+ type SpeechInputMode = "speech-recognition" | "media-recorder" | "none";
52
+ export type SpeechInputProps = ComponentProps<typeof Button> & {
53
+ onTranscriptionChange?: (text: string) => void;
54
+ onAudioRecorded?: (audioBlob: Blob) => Promise<string>;
55
+ lang?: string;
56
+ };
57
+ const detectSpeechInputMode = (): SpeechInputMode => {
58
+ if (typeof window === "undefined") {
59
+ return "none";
60
+ }
61
+ if ("SpeechRecognition" in window || "webkitSpeechRecognition" in window) {
62
+ return "speech-recognition";
63
+ }
64
+ if ("MediaRecorder" in window && "mediaDevices" in navigator) {
65
+ return "media-recorder";
66
+ }
67
+ return "none";
68
+ };
69
+ export const SpeechInput = ({
70
+ className,
71
+ onTranscriptionChange,
72
+ onAudioRecorded,
73
+ lang = "en-US",
74
+ ...props
75
+ }: SpeechInputProps) => {
76
+ const [isListening, setIsListening] = useState(false);
77
+ const [isProcessing, setIsProcessing] = useState(false);
78
+ const [mode] = useState<SpeechInputMode>(detectSpeechInputMode);
79
+ const [isRecognitionReady, setIsRecognitionReady] = useState(false);
80
+ const recognitionRef = useRef<SpeechRecognition | null>(null);
81
+ const mediaRecorderRef = useRef<MediaRecorder | null>(null);
82
+ const streamRef = useRef<MediaStream | null>(null);
83
+ const audioChunksRef = useRef<Blob[]>([]);
84
+ const onTranscriptionChangeRef = useRef<
85
+ SpeechInputProps["onTranscriptionChange"]
86
+ >(onTranscriptionChange);
87
+ const onAudioRecordedRef =
88
+ useRef<SpeechInputProps["onAudioRecorded"]>(onAudioRecorded);
89
+ onTranscriptionChangeRef.current = onTranscriptionChange;
90
+ onAudioRecordedRef.current = onAudioRecorded;
91
+ useEffect(() => {
92
+ if (mode !== "speech-recognition") {
93
+ return;
94
+ }
95
+ const SpeechRecognitionCtor =
96
+ window.SpeechRecognition || window.webkitSpeechRecognition;
97
+ const speechRecognition = new SpeechRecognitionCtor();
98
+ speechRecognition.continuous = true;
99
+ speechRecognition.interimResults = true;
100
+ speechRecognition.lang = lang;
101
+ const handleStart = () => {
102
+ setIsListening(true);
103
+ };
104
+ const handleEnd = () => {
105
+ setIsListening(false);
106
+ };
107
+ const handleResult = (event: Event) => {
108
+ const speechEvent = event as SpeechRecognitionEvent;
109
+ let finalTranscript = "";
110
+ for (
111
+ let i = speechEvent.resultIndex;
112
+ i < speechEvent.results.length;
113
+ i += 1
114
+ ) {
115
+ const result = speechEvent.results[i];
116
+ if (result?.isFinal) {
117
+ finalTranscript += result[0]?.transcript ?? "";
118
+ }
119
+ }
120
+ if (finalTranscript) {
121
+ onTranscriptionChangeRef.current?.(finalTranscript);
122
+ }
123
+ };
124
+ const handleError = () => {
125
+ setIsListening(false);
126
+ };
127
+ speechRecognition.addEventListener("start", handleStart);
128
+ speechRecognition.addEventListener("end", handleEnd);
129
+ speechRecognition.addEventListener("result", handleResult);
130
+ speechRecognition.addEventListener("error", handleError);
131
+ recognitionRef.current = speechRecognition;
132
+ setIsRecognitionReady(true);
133
+ return () => {
134
+ speechRecognition.removeEventListener("start", handleStart);
135
+ speechRecognition.removeEventListener("end", handleEnd);
136
+ speechRecognition.removeEventListener("result", handleResult);
137
+ speechRecognition.removeEventListener("error", handleError);
138
+ speechRecognition.stop();
139
+ recognitionRef.current = null;
140
+ setIsRecognitionReady(false);
141
+ };
142
+ }, [mode, lang]);
143
+ useEffect(
144
+ () => () => {
145
+ if (mediaRecorderRef.current?.state === "recording") {
146
+ mediaRecorderRef.current.stop();
147
+ }
148
+ if (streamRef.current) {
149
+ for (const track of streamRef.current.getTracks()) {
150
+ track.stop();
151
+ }
152
+ }
153
+ },
154
+ [],
155
+ );
156
+ const startMediaRecorder = async () => {
157
+ if (!onAudioRecordedRef.current) {
158
+ return;
159
+ }
160
+ try {
161
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
162
+ streamRef.current = stream;
163
+ const mediaRecorder = new MediaRecorder(stream);
164
+ audioChunksRef.current = [];
165
+ const handleDataAvailable = (event: BlobEvent) => {
166
+ if (event.data.size > 0) {
167
+ audioChunksRef.current.push(event.data);
168
+ }
169
+ };
170
+ const handleStop = async () => {
171
+ for (const track of stream.getTracks()) {
172
+ track.stop();
173
+ }
174
+ streamRef.current = null;
175
+ const audioBlob = new Blob(audioChunksRef.current, {
176
+ type: "audio/webm",
177
+ });
178
+ if (audioBlob.size > 0 && onAudioRecordedRef.current) {
179
+ setIsProcessing(true);
180
+ try {
181
+ const transcript = await onAudioRecordedRef.current(audioBlob);
182
+ if (transcript) {
183
+ onTranscriptionChangeRef.current?.(transcript);
184
+ }
185
+ } catch {
186
+ } finally {
187
+ setIsProcessing(false);
188
+ }
189
+ }
190
+ };
191
+ const handleError = () => {
192
+ setIsListening(false);
193
+ for (const track of stream.getTracks()) {
194
+ track.stop();
195
+ }
196
+ streamRef.current = null;
197
+ };
198
+ mediaRecorder.addEventListener("dataavailable", handleDataAvailable);
199
+ mediaRecorder.addEventListener("stop", handleStop);
200
+ mediaRecorder.addEventListener("error", handleError);
201
+ mediaRecorderRef.current = mediaRecorder;
202
+ mediaRecorder.start();
203
+ setIsListening(true);
204
+ } catch {
205
+ setIsListening(false);
206
+ }
207
+ };
208
+ const stopMediaRecorder = () => {
209
+ if (mediaRecorderRef.current?.state === "recording") {
210
+ mediaRecorderRef.current.stop();
211
+ }
212
+ setIsListening(false);
213
+ };
214
+ const toggleListening = () => {
215
+ if (mode === "speech-recognition" && recognitionRef.current) {
216
+ if (isListening) {
217
+ recognitionRef.current.stop();
218
+ } else {
219
+ recognitionRef.current.start();
220
+ }
221
+ } else if (mode === "media-recorder") {
222
+ if (isListening) {
223
+ stopMediaRecorder();
224
+ } else {
225
+ startMediaRecorder();
226
+ }
227
+ }
228
+ };
229
+ const isDisabled =
230
+ mode === "none" ||
231
+ (mode === "speech-recognition" && !isRecognitionReady) ||
232
+ (mode === "media-recorder" && !onAudioRecorded) ||
233
+ isProcessing;
234
+ return (
235
+ <div className="relative inline-flex items-center justify-center">
236
+ {isListening
237
+ ? [0, 1, 2].map((index) => (
238
+ <div
239
+ className="absolute inset-0 animate-ping rounded-full border-2 border-danger/30"
240
+ key={index}
241
+ style={{
242
+ animationDelay: `${index * 0.3}s`,
243
+ animationDuration: "2s",
244
+ }}
245
+ />
246
+ ))
247
+ : null}
248
+
249
+ <Button
250
+ className={cn(
251
+ "relative z-10 rounded-full transition-[background-color,transform] duration-200 ease-smooth active:duration-100 active:ease-out",
252
+ isListening
253
+ ? "bg-destructive text-danger-foreground hover:bg-destructive/80 hover:text-danger-foreground"
254
+ : "bg-primary text-primary-foreground hover:bg-primary/80 hover:text-primary-foreground",
255
+ className,
256
+ )}
257
+ disabled={isDisabled}
258
+ onClick={toggleListening}
259
+ {...props}
260
+ >
261
+ <span className="relative inline-flex size-4 items-center justify-center">
262
+ <span
263
+ className={cn(
264
+ "absolute inset-0 flex items-center justify-center transition-opacity duration-150 ease-out",
265
+ isProcessing ? "opacity-100" : "opacity-0",
266
+ )}
267
+ >
268
+ <Spinner />
269
+ </span>
270
+ <span
271
+ className={cn(
272
+ "absolute inset-0 flex items-center justify-center transition-opacity duration-150 ease-out",
273
+ !isProcessing && isListening ? "opacity-100" : "opacity-0",
274
+ )}
275
+ >
276
+ <IconSquare className="size-4" />
277
+ </span>
278
+ <span
279
+ className={cn(
280
+ "flex items-center justify-center transition-opacity duration-150 ease-out",
281
+ isProcessing || isListening ? "opacity-0" : "opacity-100",
282
+ )}
283
+ >
284
+ <IconMicrophone className="size-4" />
285
+ </span>
286
+ </span>
287
+ </Button>
288
+ </div>
289
+ );
290
+ };