@mieweb/ui 0.1.0 → 0.1.1

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 (72) hide show
  1. package/LICENSE +39 -15
  2. package/README.md +9 -1
  3. package/dist/{chunk-CLNOI5J7.js → chunk-4SMSH4OY.js} +4 -4
  4. package/dist/chunk-4SMSH4OY.js.map +1 -0
  5. package/dist/chunk-5T3AWNHG.cjs +471 -0
  6. package/dist/chunk-5T3AWNHG.cjs.map +1 -0
  7. package/dist/chunk-74K3RRU7.cjs +4 -0
  8. package/dist/{chunk-ZO46CFVN.cjs.map → chunk-74K3RRU7.cjs.map} +1 -1
  9. package/dist/{chunk-VWXGUNBR.cjs → chunk-AKTUXJPI.cjs} +107 -18
  10. package/dist/chunk-AKTUXJPI.cjs.map +1 -0
  11. package/dist/{chunk-NH2JVQ6V.cjs → chunk-I7L6CQXR.cjs} +21 -9
  12. package/dist/chunk-I7L6CQXR.cjs.map +1 -0
  13. package/dist/{chunk-KJOFWJHV.js → chunk-MFB4FS7D.js} +120 -81
  14. package/dist/chunk-MFB4FS7D.js.map +1 -0
  15. package/dist/{chunk-LEE3NMNP.cjs → chunk-NL3CZNBH.cjs} +120 -81
  16. package/dist/chunk-NL3CZNBH.cjs.map +1 -0
  17. package/dist/{chunk-BR2XGATJ.cjs → chunk-NNEFAUHV.cjs} +4 -4
  18. package/dist/chunk-NNEFAUHV.cjs.map +1 -0
  19. package/dist/chunk-SCV7C55E.cjs +11 -0
  20. package/dist/chunk-SCV7C55E.cjs.map +1 -0
  21. package/dist/{chunk-D5IBXXF2.js → chunk-SD44QJIP.js} +20 -8
  22. package/dist/chunk-SD44QJIP.js.map +1 -0
  23. package/dist/{chunk-QBWVTJKF.js → chunk-UBRDKNLQ.js} +107 -18
  24. package/dist/chunk-UBRDKNLQ.js.map +1 -0
  25. package/dist/chunk-V2DF2GUE.js +3 -0
  26. package/dist/{chunk-ZQ4XMJH7.js.map → chunk-V2DF2GUE.js.map} +1 -1
  27. package/dist/chunk-VSQF22GL.js +9 -0
  28. package/dist/chunk-VSQF22GL.js.map +1 -0
  29. package/dist/chunk-XVZ4SLQB.js +447 -0
  30. package/dist/chunk-XVZ4SLQB.js.map +1 -0
  31. package/dist/components/AudioPlayer/index.cjs +6 -6
  32. package/dist/components/AudioPlayer/index.d.cts +5 -1
  33. package/dist/components/AudioPlayer/index.d.ts +5 -1
  34. package/dist/components/AudioPlayer/index.js +1 -1
  35. package/dist/components/Modal/index.cjs +11 -10
  36. package/dist/components/Modal/index.js +3 -2
  37. package/dist/components/RecordButton/index.cjs +4 -8
  38. package/dist/components/RecordButton/index.d.cts +57 -44
  39. package/dist/components/RecordButton/index.d.ts +57 -44
  40. package/dist/components/RecordButton/index.js +1 -1
  41. package/dist/components/Select/index.cjs +3 -4
  42. package/dist/components/Select/index.js +1 -2
  43. package/dist/components/Spinner/index.d.cts +1 -1
  44. package/dist/components/Spinner/index.d.ts +1 -1
  45. package/dist/hooks/index.cjs +3 -2
  46. package/dist/hooks/index.js +2 -1
  47. package/dist/index.cjs +880 -667
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +16 -11
  50. package/dist/index.d.ts +16 -11
  51. package/dist/index.js +801 -588
  52. package/dist/index.js.map +1 -1
  53. package/dist/styles.css +1 -1
  54. package/dist/utils/index.cjs +6 -1
  55. package/dist/utils/index.d.cts +14 -1
  56. package/dist/utils/index.d.ts +14 -1
  57. package/dist/utils/index.js +2 -1
  58. package/package.json +7 -7
  59. package/dist/chunk-BR2XGATJ.cjs.map +0 -1
  60. package/dist/chunk-CLNOI5J7.js.map +0 -1
  61. package/dist/chunk-D5IBXXF2.js.map +0 -1
  62. package/dist/chunk-FQ5G7J24.js +0 -297
  63. package/dist/chunk-FQ5G7J24.js.map +0 -1
  64. package/dist/chunk-HLW3XD5R.cjs +0 -322
  65. package/dist/chunk-HLW3XD5R.cjs.map +0 -1
  66. package/dist/chunk-KJOFWJHV.js.map +0 -1
  67. package/dist/chunk-LEE3NMNP.cjs.map +0 -1
  68. package/dist/chunk-NH2JVQ6V.cjs.map +0 -1
  69. package/dist/chunk-QBWVTJKF.js.map +0 -1
  70. package/dist/chunk-VWXGUNBR.cjs.map +0 -1
  71. package/dist/chunk-ZO46CFVN.cjs +0 -4
  72. package/dist/chunk-ZQ4XMJH7.js +0 -3
package/LICENSE CHANGED
@@ -1,21 +1,45 @@
1
- MIT License
1
+ @mieweb/ui - Source Available License
2
2
 
3
- Copyright (c) 2026 MIE Web Team
3
+ Copyright (c) 2026 Medical Informatics Engineering, LLC. All rights reserved.
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ This software and associated documentation files (the "Software") are the
6
+ proprietary property of Medical Informatics Engineering, LLC. ("MIE").
11
7
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
8
+ OPEN SOURCE / NON-COMMERCIAL USE
9
+
10
+ Permission is hereby granted, free of charge, to any person or organization
11
+ obtaining a copy of this Software, to use, copy, modify, merge, and distribute
12
+ the Software for non-commercial purposes, subject to the following conditions:
13
+
14
+ 1. Attribution Required: The above copyright notice, this permission notice,
15
+ and clear attribution to Medical Informatics Engineering, LLC. shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ 2. Open Source Requirement: If you distribute the Software or any derivative
19
+ works, you must do so under an open source license approved by the Open
20
+ Source Initiative (OSI) and make the source code publicly available.
21
+
22
+ 3. Non-Commercial Use Only: This license does not grant permission to use the
23
+ Software for commercial purposes. "Commercial purposes" means any use
24
+ intended for or directed toward commercial advantage or monetary
25
+ compensation.
26
+
27
+ COMMERCIAL USE
28
+
29
+ For commercial use, including but not limited to:
30
+ - Use in proprietary/closed-source products
31
+ - Use in products or services sold for profit
32
+ - Use by for-profit organizations for internal business operations
33
+
34
+ You must obtain a separate commercial license from Medical Informatics
35
+ Engineering, LLC. Contact: https://www.mieweb.com or helpdesk@mieweb.com
36
+
37
+ NO WARRANTY
14
38
 
15
39
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
40
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
41
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
42
+ MEDICAL INFORMATICS ENGINEERING, LLC. OR ITS AFFILIATES BE LIABLE FOR ANY
43
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
44
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
45
+ OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -693,4 +693,12 @@ We welcome contributions! Here's how to get started:
693
693
 
694
694
  ## License
695
695
 
696
- MIT © MIE Web Team
696
+ Copyright © 2026 Medical Informatics Engineering, Inc. All rights reserved.
697
+
698
+ This software is **source available** with the following terms:
699
+
700
+ - ✅ **Free for open source projects** - Use, modify, and distribute freely in open source projects with attribution
701
+ - ✅ **Free for non-commercial use** - Personal projects, education, research
702
+ - 💼 **Commercial license required** - For proprietary products or commercial use, contact [licensing@mieweb.com](mailto:licensing@mieweb.com)
703
+
704
+ See the [LICENSE](LICENSE) file for full details.
@@ -1,10 +1,10 @@
1
+ import { isStorybookDocsMode } from './chunk-VSQF22GL.js';
1
2
  import { useRef, useEffect } from 'react';
2
3
 
3
- // src/hooks/useFocusTrap.ts
4
4
  function useFocusTrap(enabled = true) {
5
5
  const containerRef = useRef(null);
6
6
  useEffect(() => {
7
- if (!enabled || !containerRef.current) return;
7
+ if (!enabled || !containerRef.current || isStorybookDocsMode()) return;
8
8
  const container = containerRef.current;
9
9
  const focusableElements = container.querySelectorAll(
10
10
  'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
@@ -34,5 +34,5 @@ function useFocusTrap(enabled = true) {
34
34
  }
35
35
 
36
36
  export { useFocusTrap };
37
- //# sourceMappingURL=chunk-CLNOI5J7.js.map
38
- //# sourceMappingURL=chunk-CLNOI5J7.js.map
37
+ //# sourceMappingURL=chunk-4SMSH4OY.js.map
38
+ //# sourceMappingURL=chunk-4SMSH4OY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/useFocusTrap.ts"],"names":[],"mappings":";;;AAuBO,SAAS,YAAA,CACd,UAAU,IAAA,EACW;AACrB,EAAA,MAAM,YAAA,GAAe,OAAU,IAAI,CAAA;AAEnC,EAAA,SAAA,CAAU,MAAM;AAEd,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,YAAA,CAAa,OAAA,IAAW,qBAAoB,EAAG;AAEhE,IAAA,MAAM,YAAY,YAAA,CAAa,OAAA;AAC/B,IAAA,MAAM,oBAAoB,SAAA,CAAU,gBAAA;AAAA,MAClC;AAAA,KACF;AAEA,IAAA,IAAI,iBAAA,CAAkB,WAAW,CAAA,EAAG;AAEpC,IAAA,MAAM,YAAA,GAAe,kBAAkB,CAAC,CAAA;AACxC,IAAA,MAAM,WAAA,GAAc,iBAAA,CAAkB,iBAAA,CAAkB,MAAA,GAAS,CAAC,CAAA;AAGlE,IAAA,YAAA,CAAa,KAAA,EAAM;AAEnB,IAAA,MAAM,aAAA,GAAgB,CAAC,KAAA,KAAoC;AACzD,MAAA,IAAI,KAAA,CAAM,QAAQ,KAAA,EAAO;AAEzB,MAAA,IAAI,MAAM,QAAA,EAAU;AAElB,QAAA,IAAI,QAAA,CAAS,kBAAkB,YAAA,EAAc;AAC3C,UAAA,KAAA,CAAM,cAAA,EAAe;AACrB,UAAA,WAAA,CAAY,KAAA,EAAM;AAAA,QACpB;AAAA,MACF,CAAA,MAAO;AAEL,QAAA,IAAI,QAAA,CAAS,kBAAkB,WAAA,EAAa;AAC1C,UAAA,KAAA,CAAM,cAAA,EAAe;AACrB,UAAA,YAAA,CAAa,KAAA,EAAM;AAAA,QACrB;AAAA,MACF;AAAA,IACF,CAAA;AAEA,IAAA,SAAA,CAAU,gBAAA,CAAiB,WAAW,aAAa,CAAA;AACnD,IAAA,OAAO,MAAM,SAAA,CAAU,mBAAA,CAAoB,SAAA,EAAW,aAAa,CAAA;AAAA,EACrE,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,OAAO,YAAA;AACT","file":"chunk-4SMSH4OY.js","sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\nimport { isStorybookDocsMode } from '../utils/environment';\n\n/**\n * Hook that traps focus within a container element.\n * Essential for accessibility in modals and dialogs.\n *\n * @param enabled - Whether focus trapping is active\n * @returns ref to attach to the container element\n *\n * @example\n * ```tsx\n * function Modal({ isOpen }: { isOpen: boolean }) {\n * const containerRef = useFocusTrap<HTMLDivElement>(isOpen);\n * return (\n * <div ref={containerRef} role=\"dialog\" aria-modal=\"true\">\n * <button>First focusable</button>\n * <button>Last focusable</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useFocusTrap<T extends HTMLElement>(\n enabled = true\n): RefObject<T | null> {\n const containerRef = useRef<T>(null);\n\n useEffect(() => {\n // Skip focus trap in Storybook docs mode to prevent auto-scroll\n if (!enabled || !containerRef.current || isStorybookDocsMode()) return;\n\n const container = containerRef.current;\n const focusableElements = container.querySelectorAll<HTMLElement>(\n 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n );\n\n if (focusableElements.length === 0) return;\n\n const firstElement = focusableElements[0];\n const lastElement = focusableElements[focusableElements.length - 1];\n\n // Focus the first element when trap is enabled\n firstElement.focus();\n\n const handleKeyDown = (event: globalThis.KeyboardEvent) => {\n if (event.key !== 'Tab') return;\n\n if (event.shiftKey) {\n // Shift + Tab\n if (document.activeElement === firstElement) {\n event.preventDefault();\n lastElement.focus();\n }\n } else {\n // Tab\n if (document.activeElement === lastElement) {\n event.preventDefault();\n firstElement.focus();\n }\n }\n };\n\n container.addEventListener('keydown', handleKeyDown);\n return () => container.removeEventListener('keydown', handleKeyDown);\n }, [enabled]);\n\n return containerRef;\n}\n"]}
@@ -0,0 +1,471 @@
1
+ 'use strict';
2
+
3
+ var chunkOR5DRJCW_cjs = require('./chunk-OR5DRJCW.cjs');
4
+ var React = require('react');
5
+ var classVarianceAuthority = require('class-variance-authority');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
27
+
28
+ function MicIcon({ className }) {
29
+ return /* @__PURE__ */ jsxRuntime.jsxs(
30
+ "svg",
31
+ {
32
+ xmlns: "http://www.w3.org/2000/svg",
33
+ viewBox: "0 0 24 24",
34
+ fill: "none",
35
+ stroke: "currentColor",
36
+ strokeWidth: "2",
37
+ strokeLinecap: "round",
38
+ strokeLinejoin: "round",
39
+ className,
40
+ "aria-hidden": "true",
41
+ children: [
42
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
43
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
44
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
45
+ ]
46
+ }
47
+ );
48
+ }
49
+ function MicOffIcon({ className }) {
50
+ return /* @__PURE__ */ jsxRuntime.jsxs(
51
+ "svg",
52
+ {
53
+ xmlns: "http://www.w3.org/2000/svg",
54
+ viewBox: "0 0 24 24",
55
+ fill: "none",
56
+ stroke: "currentColor",
57
+ strokeWidth: "2",
58
+ strokeLinecap: "round",
59
+ strokeLinejoin: "round",
60
+ className,
61
+ "aria-hidden": "true",
62
+ children: [
63
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2", x2: "22", y1: "2", y2: "22" }),
64
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" }),
65
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 10v2a7 7 0 0 0 12 5" }),
66
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 9.34V5a3 3 0 0 0-5.68-1.33" }),
67
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }),
68
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
69
+ ]
70
+ }
71
+ );
72
+ }
73
+ function StopIcon({ className }) {
74
+ return /* @__PURE__ */ jsxRuntime.jsx(
75
+ "svg",
76
+ {
77
+ xmlns: "http://www.w3.org/2000/svg",
78
+ viewBox: "0 0 24 24",
79
+ fill: "currentColor",
80
+ className,
81
+ "aria-hidden": "true",
82
+ children: /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" })
83
+ }
84
+ );
85
+ }
86
+ function CheckIcon({ className }) {
87
+ return /* @__PURE__ */ jsxRuntime.jsx(
88
+ "svg",
89
+ {
90
+ xmlns: "http://www.w3.org/2000/svg",
91
+ viewBox: "0 0 24 24",
92
+ fill: "none",
93
+ stroke: "currentColor",
94
+ strokeWidth: "2.5",
95
+ strokeLinecap: "round",
96
+ strokeLinejoin: "round",
97
+ className,
98
+ "aria-hidden": "true",
99
+ children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "20 6 9 17 4 12" })
100
+ }
101
+ );
102
+ }
103
+ function LoadingSpinner({ className }) {
104
+ return /* @__PURE__ */ jsxRuntime.jsx(
105
+ "svg",
106
+ {
107
+ xmlns: "http://www.w3.org/2000/svg",
108
+ viewBox: "0 0 24 24",
109
+ fill: "none",
110
+ stroke: "currentColor",
111
+ strokeWidth: "2",
112
+ strokeLinecap: "round",
113
+ strokeLinejoin: "round",
114
+ className: chunkOR5DRJCW_cjs.cn("animate-spin", className),
115
+ "aria-hidden": "true",
116
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
117
+ }
118
+ );
119
+ }
120
+ function PulseRings({ variant }) {
121
+ const ringColor = variant === "minimal" ? "bg-red-500/30" : "bg-red-400/40";
122
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
123
+ /* @__PURE__ */ jsxRuntime.jsx(
124
+ "span",
125
+ {
126
+ className: chunkOR5DRJCW_cjs.cn("absolute inset-0 animate-ping rounded-full", ringColor),
127
+ style: { animationDuration: "1.5s" }
128
+ }
129
+ ),
130
+ /* @__PURE__ */ jsxRuntime.jsx(
131
+ "span",
132
+ {
133
+ className: chunkOR5DRJCW_cjs.cn("absolute inset-0 animate-ping rounded-full", ringColor),
134
+ style: { animationDuration: "1.5s", animationDelay: "0.5s" }
135
+ }
136
+ )
137
+ ] });
138
+ }
139
+ function WaveformBars({ size }) {
140
+ const barHeight = size === "sm" ? "h-2" : size === "md" ? "h-3" : "h-4";
141
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-0.5", children: [0, 1, 2, 3, 4].map((i) => /* @__PURE__ */ jsxRuntime.jsx(
142
+ "span",
143
+ {
144
+ className: chunkOR5DRJCW_cjs.cn(
145
+ "animate-waveform w-0.5 rounded-full bg-current",
146
+ barHeight
147
+ ),
148
+ style: {
149
+ animationDelay: `${i * 0.1}s`
150
+ }
151
+ },
152
+ i
153
+ )) });
154
+ }
155
+ var recordButtonVariants = classVarianceAuthority.cva(
156
+ [
157
+ "relative inline-flex items-center justify-center rounded-full",
158
+ "transition-all duration-200",
159
+ "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
160
+ ],
161
+ {
162
+ variants: {
163
+ variant: {
164
+ default: "",
165
+ outline: "border-2",
166
+ ghost: "",
167
+ minimal: ""
168
+ },
169
+ size: {
170
+ sm: "size-10",
171
+ md: "size-12",
172
+ lg: "size-14"
173
+ }
174
+ },
175
+ defaultVariants: {
176
+ variant: "default",
177
+ size: "md"
178
+ }
179
+ }
180
+ );
181
+ var iconSizes = {
182
+ sm: "size-4",
183
+ md: "size-5",
184
+ lg: "size-6"
185
+ };
186
+ function getStateStyles(state, variant) {
187
+ const styles = {
188
+ default: {
189
+ idle: "bg-primary/10 text-primary hover:bg-primary/20",
190
+ recording: "bg-red-500/10 text-red-500 hover:bg-red-500/20",
191
+ processing: "bg-primary/10 text-primary cursor-wait",
192
+ disabled: "bg-muted text-muted-foreground cursor-not-allowed opacity-50",
193
+ error: "bg-destructive/10 text-destructive",
194
+ success: "bg-success/10 text-success"
195
+ },
196
+ outline: {
197
+ idle: "border-primary/50 text-primary bg-transparent hover:bg-primary/10 hover:border-primary",
198
+ recording: "border-red-500/50 text-red-500 bg-transparent hover:bg-red-500/10 hover:border-red-500",
199
+ processing: "border-primary/50 text-primary bg-transparent cursor-wait",
200
+ disabled: "border-muted text-muted-foreground bg-transparent cursor-not-allowed opacity-50",
201
+ error: "border-destructive/50 text-destructive bg-transparent",
202
+ success: "border-success/50 text-success bg-transparent"
203
+ },
204
+ ghost: {
205
+ idle: "text-primary hover:bg-primary/10",
206
+ recording: "text-red-500 hover:bg-red-500/10",
207
+ processing: "text-primary bg-primary/5 cursor-wait",
208
+ disabled: "text-muted-foreground cursor-not-allowed opacity-50",
209
+ error: "text-destructive",
210
+ success: "text-success"
211
+ },
212
+ minimal: {
213
+ idle: "text-primary hover:text-primary/80",
214
+ recording: "text-red-500 hover:text-red-500/80",
215
+ processing: "text-primary cursor-wait",
216
+ disabled: "text-muted-foreground/40 cursor-not-allowed",
217
+ error: "text-destructive",
218
+ success: "text-success"
219
+ }
220
+ };
221
+ return styles[variant][state];
222
+ }
223
+ function formatDuration(seconds) {
224
+ const mins = Math.floor(seconds / 60);
225
+ const secs = Math.floor(seconds % 60);
226
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
227
+ }
228
+ var RecordButton = React__namespace.forwardRef(
229
+ ({
230
+ className,
231
+ variant = "default",
232
+ size = "md",
233
+ state: controlledState,
234
+ showWaveform = false,
235
+ showPulse = true,
236
+ disabled,
237
+ showDuration = false,
238
+ idleIcon,
239
+ recordingIcon,
240
+ transcriptionState,
241
+ showTranscriptionState = false,
242
+ onRecordingComplete,
243
+ onRecordingStart,
244
+ onRecordingError,
245
+ maxDuration = 0,
246
+ mimeType = "audio/webm",
247
+ onClick,
248
+ ...props
249
+ }, ref) => {
250
+ const [internalState, setInternalState] = React__namespace.useState("idle");
251
+ const [duration, setDuration] = React__namespace.useState(0);
252
+ const mediaRecorderRef = React__namespace.useRef(null);
253
+ const streamRef = React__namespace.useRef(null);
254
+ const chunksRef = React__namespace.useRef([]);
255
+ const timerRef = React__namespace.useRef(void 0);
256
+ const startTimeRef = React__namespace.useRef(0);
257
+ const timeoutsRef = React__namespace.useRef([]);
258
+ const addTimeout = (callback, delay) => {
259
+ const id = setTimeout(() => {
260
+ callback();
261
+ timeoutsRef.current = timeoutsRef.current.filter((t) => t !== id);
262
+ }, delay);
263
+ timeoutsRef.current.push(id);
264
+ return id;
265
+ };
266
+ const clearAllTimeouts = () => {
267
+ timeoutsRef.current.forEach(clearTimeout);
268
+ timeoutsRef.current = [];
269
+ };
270
+ const isControlled = controlledState !== void 0;
271
+ const currentState = isControlled ? controlledState : internalState;
272
+ const effectiveState = disabled ? "disabled" : transcriptionState === "error" ? "error" : transcriptionState === "transcribing" || transcriptionState === "streaming" ? "processing" : transcriptionState === "complete" ? "success" : currentState;
273
+ React__namespace.useEffect(() => {
274
+ if (typeof window === "undefined") return;
275
+ if (disabled && (controlledState && controlledState !== "disabled" || transcriptionState)) {
276
+ console.warn(
277
+ "[RecordButton]: `disabled` prop takes precedence over both `state` and `transcriptionState`. When `disabled` is true, the button will always appear disabled."
278
+ );
279
+ }
280
+ if (controlledState !== void 0 && transcriptionState !== void 0) {
281
+ const mappedTranscriptionState = transcriptionState === "error" ? "error" : transcriptionState === "transcribing" || transcriptionState === "streaming" ? "processing" : transcriptionState === "complete" ? "success" : void 0;
282
+ if (mappedTranscriptionState !== void 0 && mappedTranscriptionState !== controlledState) {
283
+ console.warn(
284
+ `[RecordButton]: \`transcriptionState\` takes precedence over \`state\`. Received state="${controlledState}" and transcriptionState="${transcriptionState}". This may lead to unexpected visual states.`
285
+ );
286
+ }
287
+ }
288
+ }, [disabled, controlledState, transcriptionState]);
289
+ const iconSize = iconSizes[size];
290
+ const isRecording = effectiveState === "recording";
291
+ const isDisabled = effectiveState === "disabled" || effectiveState === "processing";
292
+ React__namespace.useEffect(() => {
293
+ return () => {
294
+ if (timerRef.current) {
295
+ clearInterval(timerRef.current);
296
+ }
297
+ clearAllTimeouts();
298
+ if (streamRef.current) {
299
+ streamRef.current.getTracks().forEach((track) => track.stop());
300
+ }
301
+ };
302
+ }, []);
303
+ const stopRecording = React__namespace.useCallback(() => {
304
+ if (timerRef.current) {
305
+ clearInterval(timerRef.current);
306
+ }
307
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
308
+ mediaRecorderRef.current.stop();
309
+ }
310
+ if (streamRef.current) {
311
+ streamRef.current.getTracks().forEach((track) => track.stop());
312
+ }
313
+ }, []);
314
+ const startRecording = React__namespace.useCallback(async () => {
315
+ if (disabled || effectiveState === "recording" || effectiveState === "processing")
316
+ return;
317
+ try {
318
+ const stream = await navigator.mediaDevices.getUserMedia({
319
+ audio: true
320
+ });
321
+ streamRef.current = stream;
322
+ const options = { mimeType };
323
+ if (!MediaRecorder.isTypeSupported(mimeType)) {
324
+ mediaRecorderRef.current = new MediaRecorder(stream);
325
+ } else {
326
+ mediaRecorderRef.current = new MediaRecorder(stream, options);
327
+ }
328
+ chunksRef.current = [];
329
+ mediaRecorderRef.current.ondataavailable = (e) => {
330
+ if (e.data.size > 0) {
331
+ chunksRef.current.push(e.data);
332
+ }
333
+ };
334
+ mediaRecorderRef.current.onstop = () => {
335
+ if (!isControlled) {
336
+ setInternalState("processing");
337
+ }
338
+ const blob = new Blob(chunksRef.current, { type: mimeType });
339
+ const finalDuration = duration;
340
+ addTimeout(() => {
341
+ onRecordingComplete?.(blob, finalDuration);
342
+ if (!isControlled) {
343
+ setInternalState("success");
344
+ addTimeout(() => {
345
+ setInternalState("idle");
346
+ }, 1500);
347
+ }
348
+ setDuration(0);
349
+ }, 200);
350
+ };
351
+ mediaRecorderRef.current.start(100);
352
+ startTimeRef.current = Date.now();
353
+ if (!isControlled) {
354
+ setInternalState("recording");
355
+ }
356
+ onRecordingStart?.();
357
+ timerRef.current = window.setInterval(() => {
358
+ const elapsed = (Date.now() - startTimeRef.current) / 1e3;
359
+ setDuration(elapsed);
360
+ if (maxDuration > 0 && elapsed >= maxDuration) {
361
+ stopRecording();
362
+ }
363
+ }, 100);
364
+ } catch (error) {
365
+ onRecordingError?.(error);
366
+ if (!isControlled) {
367
+ setInternalState("error");
368
+ addTimeout(() => {
369
+ setInternalState("idle");
370
+ }, 2e3);
371
+ }
372
+ }
373
+ }, [
374
+ disabled,
375
+ effectiveState,
376
+ isControlled,
377
+ mimeType,
378
+ maxDuration,
379
+ duration,
380
+ onRecordingComplete,
381
+ onRecordingStart,
382
+ onRecordingError,
383
+ stopRecording
384
+ ]);
385
+ const handleClick = React__namespace.useCallback(
386
+ (e) => {
387
+ onClick?.(e);
388
+ if (!isControlled) {
389
+ if (effectiveState === "recording") {
390
+ stopRecording();
391
+ } else if (effectiveState === "idle") {
392
+ startRecording();
393
+ }
394
+ }
395
+ },
396
+ [onClick, isControlled, effectiveState, startRecording, stopRecording]
397
+ );
398
+ const renderIcon = () => {
399
+ switch (effectiveState) {
400
+ case "recording":
401
+ if (showWaveform) {
402
+ return /* @__PURE__ */ jsxRuntime.jsx(WaveformBars, { size });
403
+ }
404
+ return recordingIcon || /* @__PURE__ */ jsxRuntime.jsx(StopIcon, { className: iconSize });
405
+ case "processing":
406
+ return /* @__PURE__ */ jsxRuntime.jsx(LoadingSpinner, { className: iconSize });
407
+ case "disabled":
408
+ case "error":
409
+ return /* @__PURE__ */ jsxRuntime.jsx(MicOffIcon, { className: iconSize });
410
+ case "success":
411
+ return /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, { className: iconSize });
412
+ default:
413
+ return idleIcon || /* @__PURE__ */ jsxRuntime.jsx(MicIcon, { className: iconSize });
414
+ }
415
+ };
416
+ const getAriaLabel = () => {
417
+ switch (effectiveState) {
418
+ case "recording":
419
+ return "Stop recording";
420
+ case "processing":
421
+ return "Processing recording";
422
+ case "disabled":
423
+ return "Recording unavailable";
424
+ case "error":
425
+ return "Recording failed";
426
+ case "success":
427
+ return "Recording complete";
428
+ default:
429
+ return "Start recording";
430
+ }
431
+ };
432
+ const getTranscriptionLabel = () => {
433
+ if (transcriptionState === "streaming") return "Listening...";
434
+ if (transcriptionState === "transcribing") return "Transcribing...";
435
+ return null;
436
+ };
437
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-flex items-center gap-2", children: [
438
+ /* @__PURE__ */ jsxRuntime.jsxs(
439
+ "button",
440
+ {
441
+ ref,
442
+ type: "button",
443
+ disabled: isDisabled,
444
+ onClick: handleClick,
445
+ ...props,
446
+ className: chunkOR5DRJCW_cjs.cn(
447
+ recordButtonVariants({ variant, size }),
448
+ getStateStyles(effectiveState, variant),
449
+ className
450
+ ),
451
+ "aria-label": getAriaLabel(),
452
+ "aria-pressed": effectiveState === "recording" ? true : void 0,
453
+ "aria-busy": effectiveState === "processing" ? true : void 0,
454
+ children: [
455
+ effectiveState === "recording" && showPulse && /* @__PURE__ */ jsxRuntime.jsx(PulseRings, { variant }),
456
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative z-10", children: renderIcon() })
457
+ ]
458
+ }
459
+ ),
460
+ showDuration && isRecording && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-destructive font-mono text-xs tabular-nums", children: formatDuration(duration) }),
461
+ showTranscriptionState && getTranscriptionLabel() && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-primary text-xs font-medium", children: getTranscriptionLabel() })
462
+ ] });
463
+ }
464
+ );
465
+ RecordButton.displayName = "RecordButton";
466
+
467
+ exports.RecordButton = RecordButton;
468
+ exports.formatDuration = formatDuration;
469
+ exports.recordButtonVariants = recordButtonVariants;
470
+ //# sourceMappingURL=chunk-5T3AWNHG.cjs.map
471
+ //# sourceMappingURL=chunk-5T3AWNHG.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/RecordButton/RecordButton.tsx"],"names":["jsxs","jsx","cn","Fragment","cva","React"],"mappings":"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","file":"chunk-5T3AWNHG.cjs","sourcesContent":["import * as React from 'react';\nimport { cva } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type RecordButtonState =\n | 'idle'\n | 'recording'\n | 'processing'\n | 'disabled'\n | 'error'\n | 'success';\n\nexport type RecordButtonVariant = 'default' | 'outline' | 'ghost' | 'minimal';\nexport type RecordButtonSize = 'sm' | 'md' | 'lg';\n\n/** Transcription state for integration with transcription services */\nexport type TranscriptionState =\n | 'idle'\n | 'recording'\n | 'transcribing'\n | 'streaming'\n | 'complete'\n | 'error';\n\nexport interface TranscriptionResult {\n /** The transcribed text */\n text: string;\n /** Whether this is a partial (streaming) or final result */\n isFinal: boolean;\n /** Confidence score (0-1) if available */\n confidence?: number;\n}\n\nexport interface RecordButtonProps extends Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n 'children'\n> {\n /** Current state of the button */\n state?: RecordButtonState;\n /** Size of the button */\n size?: RecordButtonSize;\n /** Visual style variant */\n variant?: RecordButtonVariant;\n /** Show waveform bars when recording (instead of stop icon) */\n showWaveform?: boolean;\n /** Show pulse rings when recording */\n showPulse?: boolean;\n /** Show recording duration while recording */\n showDuration?: boolean;\n /** Custom idle icon */\n idleIcon?: React.ReactNode;\n /** Custom recording icon */\n recordingIcon?: React.ReactNode;\n /** Current transcription state (for external control) */\n transcriptionState?: TranscriptionState;\n /** Show transcription state indicator */\n showTranscriptionState?: boolean;\n\n // Recording callbacks (for uncontrolled usage)\n /** Callback when recording is complete with the audio blob */\n onRecordingComplete?: (blob: Blob, duration: number) => void;\n /** Callback when recording starts */\n onRecordingStart?: () => void;\n /** Callback when a recording error occurs */\n onRecordingError?: (error: Error) => void;\n /** Maximum recording duration in seconds (0 for unlimited) */\n maxDuration?: number;\n /** Audio MIME type */\n mimeType?: string;\n}\n\n// ============================================================================\n// Icons\n// ============================================================================\n\nfunction MicIcon({ className }: { className?: string }) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n aria-hidden=\"true\"\n >\n <path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\" />\n </svg>\n );\n}\n\nfunction MicOffIcon({ className }: { className?: string }) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n aria-hidden=\"true\"\n >\n <line x1=\"2\" x2=\"22\" y1=\"2\" y2=\"22\" />\n <path d=\"M18.89 13.23A7.12 7.12 0 0 0 19 12v-2\" />\n <path d=\"M5 10v2a7 7 0 0 0 12 5\" />\n <path d=\"M15 9.34V5a3 3 0 0 0-5.68-1.33\" />\n <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12\" />\n <line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\" />\n </svg>\n );\n}\n\nfunction StopIcon({ className }: { className?: string }) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"currentColor\"\n className={className}\n aria-hidden=\"true\"\n >\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" />\n </svg>\n );\n}\n\nfunction CheckIcon({ className }: { className?: string }) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n aria-hidden=\"true\"\n >\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n );\n}\n\nfunction LoadingSpinner({ className }: { className?: string }) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={cn('animate-spin', className)}\n aria-hidden=\"true\"\n >\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n );\n}\n\n// ============================================================================\n// Pulse Ring Animation (for recording state)\n// ============================================================================\n\nfunction PulseRings({ variant }: { variant: RecordButtonVariant }) {\n const ringColor = variant === 'minimal' ? 'bg-red-500/30' : 'bg-red-400/40';\n\n return (\n <>\n <span\n className={cn('absolute inset-0 animate-ping rounded-full', ringColor)}\n style={{ animationDuration: '1.5s' }}\n />\n <span\n className={cn('absolute inset-0 animate-ping rounded-full', ringColor)}\n style={{ animationDuration: '1.5s', animationDelay: '0.5s' }}\n />\n </>\n );\n}\n\n// ============================================================================\n// Waveform Animation (for recording state)\n// ============================================================================\n\nfunction WaveformBars({ size }: { size: RecordButtonSize }) {\n const barHeight = size === 'sm' ? 'h-2' : size === 'md' ? 'h-3' : 'h-4';\n\n return (\n <div className=\"flex items-center gap-0.5\">\n {[0, 1, 2, 3, 4].map((i) => (\n <span\n key={i}\n className={cn(\n 'animate-waveform w-0.5 rounded-full bg-current',\n barHeight\n )}\n style={{\n animationDelay: `${i * 0.1}s`,\n }}\n />\n ))}\n </div>\n );\n}\n\n// ============================================================================\n// Style Variants\n// ============================================================================\n\nconst recordButtonVariants = cva(\n [\n 'relative inline-flex items-center justify-center rounded-full',\n 'transition-all duration-200',\n 'outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n ],\n {\n variants: {\n variant: {\n default: '',\n outline: 'border-2',\n ghost: '',\n minimal: '',\n },\n size: {\n sm: 'size-10',\n md: 'size-12',\n lg: 'size-14',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'md',\n },\n }\n);\n\nconst iconSizes: Record<RecordButtonSize, string> = {\n sm: 'size-4',\n md: 'size-5',\n lg: 'size-6',\n};\n\n// ============================================================================\n// State Styles\n// ============================================================================\n\nfunction getStateStyles(\n state: RecordButtonState,\n variant: RecordButtonVariant\n): string {\n const styles: Record<\n RecordButtonVariant,\n Record<RecordButtonState, string>\n > = {\n default: {\n idle: 'bg-primary/10 text-primary hover:bg-primary/20',\n recording: 'bg-red-500/10 text-red-500 hover:bg-red-500/20',\n processing: 'bg-primary/10 text-primary cursor-wait',\n disabled: 'bg-muted text-muted-foreground cursor-not-allowed opacity-50',\n error: 'bg-destructive/10 text-destructive',\n success: 'bg-success/10 text-success',\n },\n outline: {\n idle: 'border-primary/50 text-primary bg-transparent hover:bg-primary/10 hover:border-primary',\n recording:\n 'border-red-500/50 text-red-500 bg-transparent hover:bg-red-500/10 hover:border-red-500',\n processing: 'border-primary/50 text-primary bg-transparent cursor-wait',\n disabled:\n 'border-muted text-muted-foreground bg-transparent cursor-not-allowed opacity-50',\n error: 'border-destructive/50 text-destructive bg-transparent',\n success: 'border-success/50 text-success bg-transparent',\n },\n ghost: {\n idle: 'text-primary hover:bg-primary/10',\n recording: 'text-red-500 hover:bg-red-500/10',\n processing: 'text-primary bg-primary/5 cursor-wait',\n disabled: 'text-muted-foreground cursor-not-allowed opacity-50',\n error: 'text-destructive',\n success: 'text-success',\n },\n minimal: {\n idle: 'text-primary hover:text-primary/80',\n recording: 'text-red-500 hover:text-red-500/80',\n processing: 'text-primary cursor-wait',\n disabled: 'text-muted-foreground/40 cursor-not-allowed',\n error: 'text-destructive',\n success: 'text-success',\n },\n };\n\n return styles[variant][state];\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\nfunction formatDuration(seconds: number): string {\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\n// ============================================================================\n// Main Component\n// ============================================================================\n\n/**\n * A voice recording button with 6 states and 4 visual variants.\n * Supports pulse animations, waveform visualization, and transcription integration.\n *\n * ## Controlled vs Uncontrolled Mode\n *\n * **Uncontrolled mode** (default): The component manages its own recording state.\n * Use `onRecordingComplete`, `onRecordingStart`, and `onRecordingError` callbacks.\n *\n * **Controlled mode**: When the `state` prop is provided, the component becomes\n * controlled and you must manage state changes externally. Note: In controlled mode,\n * the internal MediaRecorder functionality is disabled - you must implement your own\n * recording logic.\n *\n * ## State Precedence\n *\n * When multiple state-controlling props are provided, they follow this precedence:\n * 1. `disabled` prop (highest priority)\n * 2. `transcriptionState` prop\n * 3. `state` prop\n * 4. Internal state (uncontrolled)\n *\n * @example\n * ```tsx\n * // Uncontrolled with recording callbacks\n * <RecordButton\n * onRecordingComplete={(blob, duration) => console.log('Recorded:', blob)}\n * onRecordingError={(error) => console.error('Recording failed:', error)}\n * />\n *\n * // Controlled state (requires external recording implementation)\n * <RecordButton state=\"idle\" onClick={handleClick} />\n *\n * // Different variants\n * <RecordButton variant=\"outline\" size=\"lg\" />\n *\n * // With waveform animation\n * <RecordButton state=\"recording\" showWaveform showPulse />\n * ```\n */\nconst RecordButton = React.forwardRef<HTMLButtonElement, RecordButtonProps>(\n (\n {\n className,\n variant = 'default',\n size = 'md',\n state: controlledState,\n showWaveform = false,\n showPulse = true,\n disabled,\n showDuration = false,\n idleIcon,\n recordingIcon,\n transcriptionState,\n showTranscriptionState = false,\n onRecordingComplete,\n onRecordingStart,\n onRecordingError,\n maxDuration = 0,\n mimeType = 'audio/webm',\n onClick,\n ...props\n },\n ref\n ) => {\n // Internal state for uncontrolled usage\n const [internalState, setInternalState] =\n React.useState<RecordButtonState>('idle');\n const [duration, setDuration] = React.useState(0);\n\n const mediaRecorderRef = React.useRef<MediaRecorder | null>(null);\n const streamRef = React.useRef<MediaStream | null>(null);\n const chunksRef = React.useRef<Blob[]>([]);\n const timerRef = React.useRef<number | undefined>(undefined);\n const startTimeRef = React.useRef<number>(0);\n const timeoutsRef = React.useRef<NodeJS.Timeout[]>([]);\n\n // Helper to track and manage timeouts\n const addTimeout = (callback: () => void, delay: number) => {\n const id = setTimeout(() => {\n callback();\n // Remove from tracking after execution\n timeoutsRef.current = timeoutsRef.current.filter((t) => t !== id);\n }, delay);\n timeoutsRef.current.push(id);\n return id;\n };\n\n const clearAllTimeouts = () => {\n timeoutsRef.current.forEach(clearTimeout);\n timeoutsRef.current = [];\n };\n\n // Use controlled state if provided, otherwise internal state\n const isControlled = controlledState !== undefined;\n const currentState = isControlled ? controlledState : internalState;\n\n // Map transcription state to button state if provided\n // Precedence: disabled prop → transcriptionState → state prop → internal state\n const effectiveState: RecordButtonState = disabled\n ? 'disabled'\n : transcriptionState === 'error'\n ? 'error'\n : transcriptionState === 'transcribing' ||\n transcriptionState === 'streaming'\n ? 'processing'\n : transcriptionState === 'complete'\n ? 'success'\n : currentState;\n\n // Dev mode warnings for conflicting states\n React.useEffect(() => {\n // Only warn in development\n if (typeof window === 'undefined') return;\n\n // Warn when disabled is true but other state props suggest a different visual state\n if (\n disabled &&\n ((controlledState && controlledState !== 'disabled') ||\n transcriptionState)\n ) {\n console.warn(\n '[RecordButton]: `disabled` prop takes precedence over both `state` and `transcriptionState`. ' +\n 'When `disabled` is true, the button will always appear disabled.'\n );\n }\n\n // Warn when both controlled state and transcriptionState are provided and conflict\n if (controlledState !== undefined && transcriptionState !== undefined) {\n const mappedTranscriptionState: RecordButtonState | undefined =\n transcriptionState === 'error'\n ? 'error'\n : transcriptionState === 'transcribing' ||\n transcriptionState === 'streaming'\n ? 'processing'\n : transcriptionState === 'complete'\n ? 'success'\n : undefined;\n\n if (\n mappedTranscriptionState !== undefined &&\n mappedTranscriptionState !== controlledState\n ) {\n console.warn(\n '[RecordButton]: `transcriptionState` takes precedence over `state`. ' +\n `Received state=\"${controlledState}\" and transcriptionState=\"${transcriptionState}\". ` +\n 'This may lead to unexpected visual states.'\n );\n }\n }\n }, [disabled, controlledState, transcriptionState]);\n\n const iconSize = iconSizes[size];\n const isRecording = effectiveState === 'recording';\n const isDisabled =\n effectiveState === 'disabled' || effectiveState === 'processing';\n\n // Cleanup on unmount\n React.useEffect(() => {\n return () => {\n if (timerRef.current) {\n clearInterval(timerRef.current);\n }\n clearAllTimeouts();\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((track) => track.stop());\n }\n };\n }, []);\n\n const stopRecording = React.useCallback(() => {\n if (timerRef.current) {\n clearInterval(timerRef.current);\n }\n\n if (\n mediaRecorderRef.current &&\n mediaRecorderRef.current.state !== 'inactive'\n ) {\n mediaRecorderRef.current.stop();\n }\n\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((track) => track.stop());\n }\n }, []);\n\n const startRecording = React.useCallback(async () => {\n // Guard: don't start if disabled, already recording, or processing\n if (\n disabled ||\n effectiveState === 'recording' ||\n effectiveState === 'processing'\n )\n return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: true,\n });\n streamRef.current = stream;\n\n const options = { mimeType };\n if (!MediaRecorder.isTypeSupported(mimeType)) {\n mediaRecorderRef.current = new MediaRecorder(stream);\n } else {\n mediaRecorderRef.current = new MediaRecorder(stream, options);\n }\n\n chunksRef.current = [];\n\n mediaRecorderRef.current.ondataavailable = (e) => {\n if (e.data.size > 0) {\n chunksRef.current.push(e.data);\n }\n };\n\n mediaRecorderRef.current.onstop = () => {\n if (!isControlled) {\n setInternalState('processing');\n }\n\n const blob = new Blob(chunksRef.current, { type: mimeType });\n const finalDuration = duration;\n\n // Small delay to show processing state\n addTimeout(() => {\n onRecordingComplete?.(blob, finalDuration);\n if (!isControlled) {\n setInternalState('success');\n // Reset to idle after showing success\n addTimeout(() => {\n setInternalState('idle');\n }, 1500);\n }\n setDuration(0);\n }, 200);\n };\n\n mediaRecorderRef.current.start(100);\n startTimeRef.current = Date.now();\n\n if (!isControlled) {\n setInternalState('recording');\n }\n onRecordingStart?.();\n\n timerRef.current = window.setInterval(() => {\n const elapsed = (Date.now() - startTimeRef.current) / 1000;\n setDuration(elapsed);\n\n if (maxDuration > 0 && elapsed >= maxDuration) {\n stopRecording();\n }\n }, 100);\n } catch (error) {\n onRecordingError?.(error as Error);\n if (!isControlled) {\n setInternalState('error');\n // Reset to idle after showing error\n addTimeout(() => {\n setInternalState('idle');\n }, 2000);\n }\n }\n }, [\n disabled,\n effectiveState,\n isControlled,\n mimeType,\n maxDuration,\n duration,\n onRecordingComplete,\n onRecordingStart,\n onRecordingError,\n stopRecording,\n ]);\n\n const handleClick = React.useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n // Call external onClick if provided\n onClick?.(e);\n\n // Handle internal recording logic only if not fully controlled\n if (!isControlled) {\n if (effectiveState === 'recording') {\n stopRecording();\n } else if (effectiveState === 'idle') {\n startRecording();\n }\n }\n },\n [onClick, isControlled, effectiveState, startRecording, stopRecording]\n );\n\n // Determine which icon to show\n const renderIcon = () => {\n switch (effectiveState) {\n case 'recording':\n if (showWaveform) {\n return <WaveformBars size={size} />;\n }\n return recordingIcon || <StopIcon className={iconSize} />;\n case 'processing':\n return <LoadingSpinner className={iconSize} />;\n case 'disabled':\n case 'error':\n return <MicOffIcon className={iconSize} />;\n case 'success':\n return <CheckIcon className={iconSize} />;\n default:\n return idleIcon || <MicIcon className={iconSize} />;\n }\n };\n\n const getAriaLabel = () => {\n switch (effectiveState) {\n case 'recording':\n return 'Stop recording';\n case 'processing':\n return 'Processing recording';\n case 'disabled':\n return 'Recording unavailable';\n case 'error':\n return 'Recording failed';\n case 'success':\n return 'Recording complete';\n default:\n return 'Start recording';\n }\n };\n\n const getTranscriptionLabel = () => {\n if (transcriptionState === 'streaming') return 'Listening...';\n if (transcriptionState === 'transcribing') return 'Transcribing...';\n return null;\n };\n\n return (\n <div className=\"relative inline-flex items-center gap-2\">\n <button\n ref={ref}\n type=\"button\"\n disabled={isDisabled}\n onClick={handleClick}\n {...props}\n className={cn(\n recordButtonVariants({ variant, size }),\n getStateStyles(effectiveState, variant),\n className\n )}\n aria-label={getAriaLabel()}\n aria-pressed={effectiveState === 'recording' ? true : undefined}\n aria-busy={effectiveState === 'processing' ? true : undefined}\n >\n {/* Pulse animation for recording state */}\n {effectiveState === 'recording' && showPulse && (\n <PulseRings variant={variant} />\n )}\n\n {/* Icon */}\n <span className=\"relative z-10\">{renderIcon()}</span>\n </button>\n\n {/* Duration display */}\n {showDuration && isRecording && (\n <span className=\"text-destructive font-mono text-xs tabular-nums\">\n {formatDuration(duration)}\n </span>\n )}\n\n {/* Transcription state label */}\n {showTranscriptionState && getTranscriptionLabel() && (\n <span className=\"text-primary text-xs font-medium\">\n {getTranscriptionLabel()}\n </span>\n )}\n </div>\n );\n }\n);\n\nRecordButton.displayName = 'RecordButton';\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { RecordButton, recordButtonVariants, formatDuration };\n"]}