@ayasofyazilim/ui 0.0.0

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 (236) hide show
  1. package/__mocks__/canvas.ts +8 -0
  2. package/components.json +21 -0
  3. package/eslint.config.js +4 -0
  4. package/jest-environment.js +37 -0
  5. package/jest.config.ts +47 -0
  6. package/jest.setup.ts +69 -0
  7. package/package.json +124 -0
  8. package/postcss.config.mjs +6 -0
  9. package/src/aria/index.tsx +1 -0
  10. package/src/aria/number-field.tsx +41 -0
  11. package/src/components/.gitkeep +0 -0
  12. package/src/components/accordion.tsx +66 -0
  13. package/src/components/alert-dialog.tsx +157 -0
  14. package/src/components/alert.tsx +70 -0
  15. package/src/components/aspect-ratio.tsx +11 -0
  16. package/src/components/avatar.tsx +53 -0
  17. package/src/components/badge.tsx +67 -0
  18. package/src/components/breadcrumb.tsx +109 -0
  19. package/src/components/button-group.tsx +83 -0
  20. package/src/components/button.tsx +68 -0
  21. package/src/components/calendar.tsx +219 -0
  22. package/src/components/card.tsx +92 -0
  23. package/src/components/carousel.tsx +241 -0
  24. package/src/components/chart.tsx +363 -0
  25. package/src/components/checkbox.tsx +32 -0
  26. package/src/components/collapsible.tsx +33 -0
  27. package/src/components/command.tsx +184 -0
  28. package/src/components/context-menu.tsx +252 -0
  29. package/src/components/dialog.tsx +144 -0
  30. package/src/components/drawer.tsx +135 -0
  31. package/src/components/dropdown-menu.tsx +258 -0
  32. package/src/components/empty.tsx +100 -0
  33. package/src/components/field.tsx +248 -0
  34. package/src/components/form.tsx +169 -0
  35. package/src/components/hover-card.tsx +44 -0
  36. package/src/components/input-group.tsx +170 -0
  37. package/src/components/input-otp.tsx +77 -0
  38. package/src/components/input.tsx +21 -0
  39. package/src/components/item.tsx +193 -0
  40. package/src/components/kbd.tsx +28 -0
  41. package/src/components/label.tsx +24 -0
  42. package/src/components/menubar.tsx +276 -0
  43. package/src/components/navigation-menu.tsx +168 -0
  44. package/src/components/pagination.tsx +130 -0
  45. package/src/components/popover.tsx +88 -0
  46. package/src/components/progress.tsx +31 -0
  47. package/src/components/radio-group.tsx +45 -0
  48. package/src/components/resizable.tsx +56 -0
  49. package/src/components/scroll-area.tsx +58 -0
  50. package/src/components/select.tsx +189 -0
  51. package/src/components/separator.tsx +28 -0
  52. package/src/components/sheet.tsx +140 -0
  53. package/src/components/sidebar.tsx +862 -0
  54. package/src/components/skeleton.tsx +13 -0
  55. package/src/components/slider.tsx +63 -0
  56. package/src/components/sonner.tsx +40 -0
  57. package/src/components/spinner.tsx +16 -0
  58. package/src/components/stepper.tsx +291 -0
  59. package/src/components/switch.tsx +31 -0
  60. package/src/components/table.tsx +133 -0
  61. package/src/components/tabs.tsx +66 -0
  62. package/src/components/textarea.tsx +18 -0
  63. package/src/components/toggle-group.tsx +83 -0
  64. package/src/components/toggle.tsx +47 -0
  65. package/src/components/tooltip.tsx +66 -0
  66. package/src/custom/action-button.tsx +48 -0
  67. package/src/custom/async-select.tsx +287 -0
  68. package/src/custom/awesome-not-found.tsx +116 -0
  69. package/src/custom/charts/area-chart.tsx +147 -0
  70. package/src/custom/charts/bar-chart.tsx +233 -0
  71. package/src/custom/charts/chart-card.tsx +103 -0
  72. package/src/custom/charts/index.tsx +16 -0
  73. package/src/custom/charts/pie-chart.tsx +168 -0
  74. package/src/custom/charts/radar-chart.tsx +126 -0
  75. package/src/custom/checkbox-tree.tsx +100 -0
  76. package/src/custom/combobox.tsx +296 -0
  77. package/src/custom/confirm-dialog.tsx +102 -0
  78. package/src/custom/country-selector.tsx +204 -0
  79. package/src/custom/date-picker/calendar-rac.tsx +109 -0
  80. package/src/custom/date-picker/datefield-rac.tsx +84 -0
  81. package/src/custom/date-picker/index.tsx +273 -0
  82. package/src/custom/date-picker/types/index.ts +4 -0
  83. package/src/custom/date-picker/utils/index.ts +42 -0
  84. package/src/custom/date-picker-old.tsx +50 -0
  85. package/src/custom/date-tooltip.tsx +98 -0
  86. package/src/custom/document-scanner/consts.ts +5 -0
  87. package/src/custom/document-scanner/corner-adjustment/action-buttons.tsx +33 -0
  88. package/src/custom/document-scanner/corner-adjustment/corner-handle.tsx +43 -0
  89. package/src/custom/document-scanner/corner-adjustment/hooks/use-corner-drag.ts +85 -0
  90. package/src/custom/document-scanner/corner-adjustment/index.tsx +125 -0
  91. package/src/custom/document-scanner/corner-adjustment/types.ts +53 -0
  92. package/src/custom/document-scanner/corner-adjustment/utils/clip-path.ts +22 -0
  93. package/src/custom/document-scanner/corner-adjustment/zoom-magnifier.tsx +115 -0
  94. package/src/custom/document-scanner/hooks/use-document-capture.ts +81 -0
  95. package/src/custom/document-scanner/hooks/use-document-scanner.ts +80 -0
  96. package/src/custom/document-scanner/hooks/use-perspective-crop.ts +38 -0
  97. package/src/custom/document-scanner/index.tsx +255 -0
  98. package/src/custom/document-scanner/lib.ts +407 -0
  99. package/src/custom/document-scanner/types.ts +205 -0
  100. package/src/custom/document-scanner/utils/perspective-correction.ts +139 -0
  101. package/src/custom/document-viewer/controllers.tsx +98 -0
  102. package/src/custom/document-viewer/index.tsx +43 -0
  103. package/src/custom/document-viewer/renderers/image.tsx +37 -0
  104. package/src/custom/document-viewer/renderers/index.tsx +2 -0
  105. package/src/custom/document-viewer/renderers/pdf.tsx +105 -0
  106. package/src/custom/email-input/domains.json +159 -0
  107. package/src/custom/email-input/email.tsx +229 -0
  108. package/src/custom/email-input/index.tsx +4 -0
  109. package/src/custom/email-input/types.ts +104 -0
  110. package/src/custom/file-uploader.tsx +541 -0
  111. package/src/custom/filter-component/fields/async-select.tsx +33 -0
  112. package/src/custom/filter-component/fields/date.tsx +60 -0
  113. package/src/custom/filter-component/fields/multi-select.tsx +30 -0
  114. package/src/custom/filter-component/index.tsx +217 -0
  115. package/src/custom/image-canvas.tsx +260 -0
  116. package/src/custom/json-editor.tsx +22 -0
  117. package/src/custom/master-data-grid/components/dialogs/column-settings-dialog.tsx +100 -0
  118. package/src/custom/master-data-grid/components/dialogs/index.ts +1 -0
  119. package/src/custom/master-data-grid/components/filters/client-filter.tsx +368 -0
  120. package/src/custom/master-data-grid/components/filters/filter-input.tsx +256 -0
  121. package/src/custom/master-data-grid/components/filters/index.ts +3 -0
  122. package/src/custom/master-data-grid/components/filters/inline-column-filter.tsx +233 -0
  123. package/src/custom/master-data-grid/components/filters/multi-filter-dialog.tsx +90 -0
  124. package/src/custom/master-data-grid/components/filters/server-filter.tsx +255 -0
  125. package/src/custom/master-data-grid/components/master-data-grid.tsx +472 -0
  126. package/src/custom/master-data-grid/components/pagination/index.ts +1 -0
  127. package/src/custom/master-data-grid/components/pagination/pagination.tsx +178 -0
  128. package/src/custom/master-data-grid/components/table/cell-renderer.tsx +634 -0
  129. package/src/custom/master-data-grid/components/table/header-cell.tsx +162 -0
  130. package/src/custom/master-data-grid/components/table/index.ts +4 -0
  131. package/src/custom/master-data-grid/components/table/table-body-renderer.tsx +113 -0
  132. package/src/custom/master-data-grid/components/table/virtual-body.tsx +138 -0
  133. package/src/custom/master-data-grid/components/toolbar/index.ts +1 -0
  134. package/src/custom/master-data-grid/components/toolbar/toolbar.tsx +314 -0
  135. package/src/custom/master-data-grid/hooks/index.ts +3 -0
  136. package/src/custom/master-data-grid/hooks/use-columns.tsx +332 -0
  137. package/src/custom/master-data-grid/hooks/use-editing.ts +106 -0
  138. package/src/custom/master-data-grid/hooks/use-table-state-reducer.ts +157 -0
  139. package/src/custom/master-data-grid/hooks/use-table-state.ts +31 -0
  140. package/src/custom/master-data-grid/index.ts +16 -0
  141. package/src/custom/master-data-grid/types.ts +466 -0
  142. package/src/custom/master-data-grid/utils/column-generator.tsx +306 -0
  143. package/src/custom/master-data-grid/utils/export-utils.ts +67 -0
  144. package/src/custom/master-data-grid/utils/filter-fns.ts +290 -0
  145. package/src/custom/master-data-grid/utils/index.ts +8 -0
  146. package/src/custom/master-data-grid/utils/pinning-utils.ts +88 -0
  147. package/src/custom/master-data-grid/utils/translation-utils.ts +42 -0
  148. package/src/custom/multi-select.tsx +432 -0
  149. package/src/custom/password-input.tsx +194 -0
  150. package/src/custom/phone-input.tsx +172 -0
  151. package/src/custom/schema-form/custom/index.tsx +1 -0
  152. package/src/custom/schema-form/custom/label.tsx +53 -0
  153. package/src/custom/schema-form/fields/base-input-field.tsx +82 -0
  154. package/src/custom/schema-form/fields/field.tsx +67 -0
  155. package/src/custom/schema-form/fields/index.tsx +5 -0
  156. package/src/custom/schema-form/fields/object.tsx +12 -0
  157. package/src/custom/schema-form/fields/table-array/array-field-item.tsx +90 -0
  158. package/src/custom/schema-form/fields/table-array/array-field-template.tsx +115 -0
  159. package/src/custom/schema-form/index.tsx +259 -0
  160. package/src/custom/schema-form/templates/description.tsx +20 -0
  161. package/src/custom/schema-form/templates/index.tsx +2 -0
  162. package/src/custom/schema-form/templates/submit.tsx +32 -0
  163. package/src/custom/schema-form/types.ts +64 -0
  164. package/src/custom/schema-form/utils/index.ts +4 -0
  165. package/src/custom/schema-form/utils/schema-dependency.ts +655 -0
  166. package/src/custom/schema-form/utils/schemas.ts +289 -0
  167. package/src/custom/schema-form/utils/validation.ts +23 -0
  168. package/src/custom/schema-form/widgets/boolean.tsx +77 -0
  169. package/src/custom/schema-form/widgets/combobox.tsx +274 -0
  170. package/src/custom/schema-form/widgets/date.tsx +59 -0
  171. package/src/custom/schema-form/widgets/email.tsx +34 -0
  172. package/src/custom/schema-form/widgets/index.tsx +10 -0
  173. package/src/custom/schema-form/widgets/password.tsx +40 -0
  174. package/src/custom/schema-form/widgets/phone.tsx +40 -0
  175. package/src/custom/schema-form/widgets/select.tsx +105 -0
  176. package/src/custom/schema-form/widgets/selectable.tsx +25 -0
  177. package/src/custom/schema-form/widgets/string-array.tsx +296 -0
  178. package/src/custom/schema-form/widgets/url.tsx +56 -0
  179. package/src/custom/section-layout-v2.tsx +212 -0
  180. package/src/custom/select-tabs.tsx +109 -0
  181. package/src/custom/selectable.tsx +316 -0
  182. package/src/custom/stepper.tsx +236 -0
  183. package/src/custom/tab-layout.tsx +213 -0
  184. package/src/custom/tanstack-table/fields/index.tsx +12 -0
  185. package/src/custom/tanstack-table/fields/tanstack-table-action-dialogs.tsx +89 -0
  186. package/src/custom/tanstack-table/fields/tanstack-table-column-header.tsx +66 -0
  187. package/src/custom/tanstack-table/fields/tanstack-table-filter-date.tsx +180 -0
  188. package/src/custom/tanstack-table/fields/tanstack-table-filter-faceted.tsx +158 -0
  189. package/src/custom/tanstack-table/fields/tanstack-table-filter-text.tsx +76 -0
  190. package/src/custom/tanstack-table/fields/tanstack-table-pagination.tsx +136 -0
  191. package/src/custom/tanstack-table/fields/tanstack-table-plain-table.tsx +142 -0
  192. package/src/custom/tanstack-table/fields/tanstack-table-row-actions-confirmation.tsx +77 -0
  193. package/src/custom/tanstack-table/fields/tanstack-table-row-actions-custom-dialog.tsx +87 -0
  194. package/src/custom/tanstack-table/fields/tanstack-table-row-actions.tsx +151 -0
  195. package/src/custom/tanstack-table/fields/tanstack-table-table-actions-custom-dialog.tsx +88 -0
  196. package/src/custom/tanstack-table/fields/tanstack-table-table-actions-schemaform-dialog.tsx +47 -0
  197. package/src/custom/tanstack-table/fields/tanstack-table-toolbar.tsx +143 -0
  198. package/src/custom/tanstack-table/fields/tanstack-table-view-options.tsx +171 -0
  199. package/src/custom/tanstack-table/index.tsx +244 -0
  200. package/src/custom/tanstack-table/types/index.ts +328 -0
  201. package/src/custom/tanstack-table/utils/cell-with-actions.tsx +21 -0
  202. package/src/custom/tanstack-table/utils/column-names.ts +26 -0
  203. package/src/custom/tanstack-table/utils/columns-by-row-data.tsx +312 -0
  204. package/src/custom/tanstack-table/utils/editable-columns-by-row-data.tsx +219 -0
  205. package/src/custom/tanstack-table/utils/faceted-boolean-options.tsx +22 -0
  206. package/src/custom/tanstack-table/utils/index.tsx +10 -0
  207. package/src/custom/tanstack-table/utils/pinning-styles.ts +57 -0
  208. package/src/custom/tanstack-table/utils/table.tsx +83 -0
  209. package/src/custom/tanstack-table/utils/test-conditions.ts +17 -0
  210. package/src/custom/timeline.tsx +208 -0
  211. package/src/custom/tree.tsx +200 -0
  212. package/src/custom/tscanify/browser.ts +66 -0
  213. package/src/custom/tscanify/index.ts +51 -0
  214. package/src/custom/tscanify/tscanify-browser.ts +522 -0
  215. package/src/custom/tscanify/tscanify.ts +262 -0
  216. package/src/custom/tscanify/types.ts +22 -0
  217. package/src/custom/webcam.tsx +737 -0
  218. package/src/hooks/.gitkeep +0 -0
  219. package/src/hooks/use-callback-ref.ts +27 -0
  220. package/src/hooks/use-controllable-state.ts +67 -0
  221. package/src/hooks/use-debounce.ts +19 -0
  222. package/src/hooks/use-is-visible.ts +23 -0
  223. package/src/hooks/use-media-query.ts +21 -0
  224. package/src/hooks/use-mobile.ts +21 -0
  225. package/src/hooks/use-on-window-resize.ts +15 -0
  226. package/src/hooks/use-scroll.tsx +22 -0
  227. package/src/lib/utils.ts +61 -0
  228. package/src/lib/zod.ts +2 -0
  229. package/src/styles/core.css +57 -0
  230. package/src/styles/globals.css +130 -0
  231. package/src/test/email-input.test.tsx +217 -0
  232. package/src/test/password-input.test.tsx +92 -0
  233. package/src/test/select-tabs.test.tsx +302 -0
  234. package/src/test/selectable.test.tsx +1093 -0
  235. package/tsconfig.json +13 -0
  236. package/tsconfig.lint.json +8 -0
@@ -0,0 +1,737 @@
1
+ /**
2
+ * Webcam Component - Core Features
3
+ *
4
+ * Three main functionalities:
5
+ * 1) Record Video - with interface controls, auto-start, max duration
6
+ * 2) Capture Photo - with interface controls, quality settings
7
+ * 3) Auto-capture Photo - with interface controls, intervals, quality
8
+ *
9
+ * Features:
10
+ * - Customizable placeholder overlay
11
+ * - Comprehensive feedback functions
12
+ * - Camera switching support
13
+ * - Internationalization support
14
+ */
15
+
16
+ "use client";
17
+
18
+ import { Camera, Pause, Play, RefreshCw, Square, Video } from "lucide-react";
19
+ import * as React from "react";
20
+ import { useCallback, useEffect, useRef, useState, useTransition } from "react";
21
+ import WebcamCore from "react-webcam";
22
+ import { cn } from "@repo/ayasofyazilim-ui/lib/utils";
23
+ import { Button } from "@repo/ayasofyazilim-ui/components/button";
24
+
25
+ // Types
26
+ export interface VideoDimensions {
27
+ width: number;
28
+ height: number;
29
+ }
30
+
31
+ // Core Feature Interfaces
32
+ export interface VideoRecordingConfig {
33
+ hasInterface: boolean; // Show video recording UI controls
34
+ autoStart?: boolean; // Auto-start recording when camera ready
35
+ maxVideoDuration?: number; // Max duration in seconds (default: 60)
36
+ quality?: "low" | "medium" | "high"; // Video quality
37
+ mimeType?: string; // Video MIME type (default: 'video/webm')
38
+ }
39
+
40
+ export interface PhotoCaptureConfig {
41
+ hasInterface: boolean; // Show photo capture UI controls
42
+ quality?: number; // JPEG quality 0-1 (default: 0.95)
43
+ }
44
+
45
+ export interface AutoCaptureConfig {
46
+ hasInterface: boolean; // Show auto-capture UI controls
47
+ captureInterval: number; // Interval in seconds
48
+ startOnReady?: boolean; // Auto-start when camera ready (only on initial load)
49
+ quality?: number; // JPEG quality 0-1 (default: 0.95)
50
+ stopOnCapture?: boolean; // Stop auto capture after first successful capture
51
+ }
52
+
53
+ // Feedback Functions
54
+ export interface WebcamCallbacks {
55
+ onWebcamReady?: (dimensions: VideoDimensions) => void;
56
+ onVideoStart?: () => void;
57
+ onVideoEnd?: (videoBlob: Blob, duration: number) => void;
58
+ onPhotoCaptured?: (imageData: string) => void;
59
+ onAutoPhotoCaptured?: (imageData: string) => void;
60
+ onAutoCaptureStart?: () => void;
61
+ beforeStartAutoCapture?: () => void;
62
+ onAutoCaptureStop?: () => void;
63
+ onError?: (error: string) => void;
64
+ }
65
+
66
+ // Main Props Interface
67
+ export interface WebcamProps {
68
+ // Core Features (at least one must be enabled)
69
+ videoRecording?: VideoRecordingConfig;
70
+ photoCapture?: PhotoCaptureConfig;
71
+ autoCapture?: AutoCaptureConfig;
72
+
73
+ // Camera Settings
74
+ defaultCamera?: "front" | "back"; // Default camera (front='user', back='environment')
75
+ allowCameraSwitch?: boolean; // Allow camera switching
76
+
77
+ // UI Customization
78
+ placeholder?: React.ReactElement; // Overlay that covers camera area
79
+ showCapturedImage?: boolean; // Show last captured image preview
80
+ capturedImage?: string | null; // Current captured image for preview
81
+ forceHideInterface?: boolean; // Force hide interface controls (useful for manual capture mode)
82
+ // Feedback Functions
83
+ callbacks?: WebcamCallbacks;
84
+
85
+ // Labels for Translation
86
+ labels?: {
87
+ // Video Recording
88
+ startRecording?: string;
89
+ stopRecording?: string;
90
+ recording?: string;
91
+
92
+ // Photo Capture
93
+ capturePhoto?: string;
94
+
95
+ // Auto Capture
96
+ startAutoCapture?: string;
97
+ stopAutoCapture?: string;
98
+ autoCapturing?: string;
99
+
100
+ // General
101
+ switchCamera?: string;
102
+ cameraReady?: string;
103
+ cameraError?: string;
104
+ };
105
+
106
+ // Styling
107
+ classNames?: {
108
+ container?: string;
109
+ webcam?: string;
110
+ placeholder?: string;
111
+ controls?: string;
112
+ previewImage?: string;
113
+ };
114
+
115
+ // Technical Settings
116
+ webcamRef?: React.RefObject<WebcamCore>;
117
+ videoCheckInterval?: number; // default: 500ms
118
+ maxRetryCount?: number; // default: 10
119
+ interfaceLocation?: "static" | "absolute"; // Location of controls (default: 'bottom')
120
+ showBorder?: boolean; // Show border around webcam area (default: true)
121
+ }
122
+
123
+ export type WebcamCaptureProps = WebcamProps;
124
+
125
+ // Legacy aliases for backward compatibility
126
+ export type WebcamAutoCapture = WebcamProps;
127
+ export type WebcamManualCapture = WebcamProps;
128
+
129
+ export function Webcam(props: WebcamProps) {
130
+ const {
131
+ videoRecording,
132
+ photoCapture,
133
+ autoCapture,
134
+ defaultCamera = "front",
135
+ allowCameraSwitch = false,
136
+ placeholder,
137
+ showCapturedImage = false,
138
+ capturedImage,
139
+ callbacks,
140
+ labels = {},
141
+ classNames,
142
+ webcamRef: externalWebcamRef,
143
+ videoCheckInterval = 500,
144
+ maxRetryCount = 10,
145
+ showBorder = false,
146
+ forceHideInterface = false,
147
+ interfaceLocation = "static",
148
+ } = props;
149
+
150
+ // Validation: At least one feature must be enabled
151
+ if (!videoRecording && !photoCapture && !autoCapture) {
152
+ throw new Error(
153
+ "WebcamComponent: At least one core feature (videoRecording, photoCapture, or autoCapture) must be enabled"
154
+ );
155
+ }
156
+
157
+ // Default labels
158
+ const defaultLabels = {
159
+ startRecording: "Start Recording",
160
+ stopRecording: "Stop Recording",
161
+ recording: "Recording...",
162
+ capturePhoto: "Capture Photo",
163
+ startAutoCapture: "Start Auto Capture",
164
+ stopAutoCapture: "Stop Auto Capture",
165
+ autoCapturing: "Auto Capturing...",
166
+ switchCamera: "Switch Camera",
167
+ cameraReady: "Camera Ready",
168
+ cameraError: "Camera Error",
169
+ ...labels,
170
+ };
171
+
172
+ // State
173
+ const [isPending, startTransition] = useTransition();
174
+ const [facingMode, setFacingMode] = useState<"user" | "environment">(
175
+ defaultCamera === "front" ? "user" : "environment"
176
+ );
177
+ const [isWebcamReady, setIsWebcamReady] = useState(false);
178
+ // Video Recording State
179
+ const [isRecording, setIsRecording] = useState(false);
180
+ const [recordingDuration, setRecordingDuration] = useState(0);
181
+
182
+ // Auto Capture State
183
+ const [isAutoCapturing, setIsAutoCapturing] = useState(false);
184
+
185
+ // Auto capture state to track if it has been initially started
186
+ const [hasAutoStarted, setHasAutoStarted] = useState(false);
187
+
188
+ // Refs
189
+ const internalWebcamRef = useRef<WebcamCore>(null);
190
+ const webcamRef = externalWebcamRef || internalWebcamRef;
191
+ const videoCheckIntervalRef = useRef<NodeJS.Timeout | null>(null);
192
+ const autoCapturIntervalRef = useRef<NodeJS.Timeout | null>(null);
193
+ const recordingIntervalRef = useRef<NodeJS.Timeout | null>(null);
194
+ const retryCountRef = useRef(0);
195
+ const mediaRecorderRef = useRef<MediaRecorder | null>(null);
196
+ const recordedChunksRef = useRef<Blob[]>([]);
197
+
198
+ // Cleanup utilities
199
+ const clearVideoCheckInterval = useCallback(() => {
200
+ if (videoCheckIntervalRef.current) {
201
+ clearInterval(videoCheckIntervalRef.current);
202
+ videoCheckIntervalRef.current = null;
203
+ }
204
+ }, []);
205
+
206
+ const clearAutoCapture = useCallback(() => {
207
+ if (autoCapturIntervalRef.current) {
208
+ clearInterval(autoCapturIntervalRef.current);
209
+ autoCapturIntervalRef.current = null;
210
+ }
211
+ }, []);
212
+
213
+ const clearRecordingInterval = useCallback(() => {
214
+ if (recordingIntervalRef.current) {
215
+ clearInterval(recordingIntervalRef.current);
216
+ recordingIntervalRef.current = null;
217
+ }
218
+ }, []);
219
+
220
+ // Video Recording Functions
221
+ const startVideoRecording = useCallback(() => {
222
+ if (!webcamRef.current?.stream || isRecording || !videoRecording) return;
223
+
224
+ try {
225
+ const { stream } = webcamRef.current;
226
+ const mimeType = videoRecording.mimeType || "video/webm";
227
+
228
+ const mediaRecorder = new MediaRecorder(stream, { mimeType });
229
+ recordedChunksRef.current = [];
230
+
231
+ mediaRecorder.ondataavailable = (event) => {
232
+ if (event.data.size > 0) {
233
+ recordedChunksRef.current.push(event.data);
234
+ }
235
+ };
236
+
237
+ mediaRecorder.onstop = () => {
238
+ const blob = new Blob(recordedChunksRef.current, { type: mimeType });
239
+ const duration = recordingDuration;
240
+
241
+ callbacks?.onVideoEnd?.(blob, duration);
242
+ setIsRecording(false);
243
+ setRecordingDuration(0);
244
+ clearRecordingInterval();
245
+ };
246
+
247
+ mediaRecorderRef.current = mediaRecorder;
248
+ mediaRecorder.start();
249
+
250
+ const startTime = Date.now();
251
+ setIsRecording(true);
252
+ setRecordingDuration(0);
253
+
254
+ // Start duration tracking
255
+ recordingIntervalRef.current = setInterval(() => {
256
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
257
+ setRecordingDuration(elapsed);
258
+
259
+ // Auto-stop if max duration reached
260
+ if (
261
+ videoRecording.maxVideoDuration &&
262
+ elapsed >= videoRecording.maxVideoDuration
263
+ ) {
264
+ stopVideoRecording();
265
+ }
266
+ }, 1000);
267
+
268
+ callbacks?.onVideoStart?.();
269
+ } catch (error) {
270
+ callbacks?.onError?.(`Failed to start video recording: ${error}`);
271
+ }
272
+ }, [
273
+ webcamRef,
274
+ isRecording,
275
+ videoRecording,
276
+ callbacks,
277
+ recordingDuration,
278
+ clearRecordingInterval,
279
+ ]);
280
+
281
+ const stopVideoRecording = useCallback(() => {
282
+ if (mediaRecorderRef.current && isRecording) {
283
+ mediaRecorderRef.current.stop();
284
+ mediaRecorderRef.current = null;
285
+ }
286
+ }, [isRecording]);
287
+
288
+ // Photo Capture Functions
289
+ const captureHighQualityPhoto = useCallback((): string | null => {
290
+ const video = webcamRef.current?.video;
291
+ if (!video) return null;
292
+
293
+ const canvas = document.createElement("canvas");
294
+ const ctx = canvas.getContext("2d");
295
+ if (!ctx) return null;
296
+
297
+ canvas.width = video.videoWidth;
298
+ canvas.height = video.videoHeight;
299
+
300
+ ctx.imageSmoothingEnabled = true;
301
+ ctx.imageSmoothingQuality = "high";
302
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
303
+
304
+ const quality = photoCapture?.quality || 0.95;
305
+ return canvas.toDataURL("image/jpeg", quality);
306
+ }, [webcamRef, photoCapture]);
307
+
308
+ const capturePhoto = useCallback(() => {
309
+ if (!photoCapture) return;
310
+
311
+ const imageData = captureHighQualityPhoto();
312
+ if (imageData) {
313
+ callbacks?.onPhotoCaptured?.(imageData);
314
+ }
315
+ }, [photoCapture, captureHighQualityPhoto, callbacks]);
316
+
317
+ const stopAutoCapture = useCallback(() => {
318
+ setIsAutoCapturing(false);
319
+ callbacks?.onAutoCaptureStop?.();
320
+ clearAutoCapture();
321
+ }, [clearAutoCapture, callbacks]);
322
+
323
+ // Auto Capture Functions
324
+ const captureAutoPhoto = useCallback(() => {
325
+ if (!autoCapture) return;
326
+
327
+ const video = webcamRef.current?.video;
328
+ if (!video) return;
329
+
330
+ const canvas = document.createElement("canvas");
331
+ const ctx = canvas.getContext("2d");
332
+ if (!ctx) return;
333
+
334
+ canvas.width = video.videoWidth;
335
+ canvas.height = video.videoHeight;
336
+
337
+ ctx.imageSmoothingEnabled = true;
338
+ ctx.imageSmoothingQuality = "high";
339
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
340
+
341
+ const quality = autoCapture.quality || 0.95;
342
+ const imageData = canvas.toDataURL("image/jpeg", quality);
343
+
344
+ if (imageData) {
345
+ callbacks?.onAutoPhotoCaptured?.(imageData);
346
+
347
+ // Stop auto capture if stopOnCapture is enabled
348
+ if (autoCapture.stopOnCapture) {
349
+ stopAutoCapture();
350
+ }
351
+ }
352
+ }, [webcamRef, autoCapture, callbacks, stopAutoCapture]);
353
+
354
+ const startAutoCapture = useCallback(
355
+ (isManual = false) => {
356
+ if (!autoCapture || !isWebcamReady || isAutoCapturing) return;
357
+
358
+ // If it's automatic start (startOnReady) and already started, don't start again
359
+ if (!isManual && autoCapture.startOnReady && hasAutoStarted) return;
360
+
361
+ setIsAutoCapturing(true);
362
+ callbacks?.onAutoCaptureStart?.();
363
+
364
+ // Set hasAutoStarted flag only for automatic starts
365
+ if (!isManual && autoCapture.startOnReady) {
366
+ setHasAutoStarted(true);
367
+ }
368
+
369
+ autoCapturIntervalRef.current = setInterval(() => {
370
+ if (webcamRef.current && isWebcamReady) {
371
+ captureAutoPhoto();
372
+ }
373
+ }, autoCapture.captureInterval * 1000);
374
+ },
375
+ [
376
+ autoCapture,
377
+ isWebcamReady,
378
+ isAutoCapturing,
379
+ captureAutoPhoto,
380
+ webcamRef,
381
+ callbacks,
382
+ hasAutoStarted,
383
+ ]
384
+ );
385
+
386
+ // Camera Management
387
+ const switchCamera = useCallback(() => {
388
+ const newFacingMode = facingMode === "user" ? "environment" : "user";
389
+ setFacingMode(newFacingMode);
390
+ setIsWebcamReady(false);
391
+ setHasAutoStarted(false); // Reset auto-start flag for new camera
392
+ stopAutoCapture();
393
+ }, [facingMode, stopAutoCapture]);
394
+
395
+ // Add device detection for better browser compatibility
396
+ const [isMobile, setIsMobile] = useState(false);
397
+
398
+ // Detect mobile device on component mount
399
+ useEffect(() => {
400
+ const checkIsMobile = () =>
401
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
402
+ navigator.userAgent
403
+ );
404
+ setIsMobile(checkIsMobile());
405
+ }, []);
406
+
407
+ // Video Readiness Check
408
+ const checkVideoReady = useCallback(() => {
409
+ const video = webcamRef.current?.video;
410
+ if (!video) return false;
411
+
412
+ const isReady =
413
+ video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0;
414
+
415
+ if (isReady) {
416
+ const newDimensions: VideoDimensions = {
417
+ width: video.videoWidth,
418
+ height: video.videoHeight,
419
+ };
420
+ callbacks?.onWebcamReady?.(newDimensions);
421
+ }
422
+
423
+ return isReady;
424
+ }, [webcamRef, callbacks]);
425
+
426
+ // Video Initialization
427
+ const handleUserMedia = useCallback(() => {
428
+ clearVideoCheckInterval();
429
+ retryCountRef.current = 0;
430
+
431
+ videoCheckIntervalRef.current = setInterval(() => {
432
+ if (checkVideoReady()) {
433
+ setIsWebcamReady(true);
434
+ clearVideoCheckInterval();
435
+
436
+ // Auto-start features if enabled
437
+ if (videoRecording?.autoStart) {
438
+ startVideoRecording();
439
+ }
440
+
441
+ if (autoCapture?.startOnReady && !hasAutoStarted) {
442
+ setTimeout(() => {
443
+ startAutoCapture(false); // Automatic start
444
+ }, 100);
445
+ }
446
+ } else {
447
+ retryCountRef.current++;
448
+
449
+ if (retryCountRef.current > maxRetryCount) {
450
+ clearVideoCheckInterval();
451
+ callbacks?.onError?.(
452
+ "Failed to initialize camera after maximum retries"
453
+ );
454
+
455
+ // Try with default constraints as a fallback
456
+ const currentFacingMode = facingMode;
457
+ setFacingMode("user");
458
+ setTimeout(() => setFacingMode(currentFacingMode), 100);
459
+ }
460
+ }
461
+ }, videoCheckInterval);
462
+
463
+ return clearVideoCheckInterval;
464
+ }, [
465
+ clearVideoCheckInterval,
466
+ checkVideoReady,
467
+ videoRecording,
468
+ autoCapture,
469
+ startVideoRecording,
470
+ startAutoCapture,
471
+ maxRetryCount,
472
+ videoCheckInterval,
473
+ facingMode,
474
+ callbacks,
475
+ ]);
476
+
477
+ // Handle camera initialization errors
478
+ const handleUserMediaError = useCallback(
479
+ (error: string | DOMException) => {
480
+ callbacks?.onError?.(
481
+ typeof error === "string" ? error : `Camera error: ${error.message}`
482
+ );
483
+ setIsWebcamReady(false);
484
+ },
485
+ [callbacks]
486
+ );
487
+
488
+ // Manual capture function
489
+ const handleCapturePhoto = useCallback(() => {
490
+ startTransition(() => {
491
+ capturePhoto();
492
+ });
493
+ }, [capturePhoto, startTransition]);
494
+
495
+ // Manual auto capture start function for button
496
+ const handleStartAutoCapture = useCallback(() => {
497
+ if (callbacks?.beforeStartAutoCapture) callbacks.beforeStartAutoCapture();
498
+ startAutoCapture(true); // Manual start
499
+ }, [startAutoCapture, callbacks]);
500
+
501
+ // Effects
502
+ useEffect(
503
+ () => () => {
504
+ clearVideoCheckInterval();
505
+ stopAutoCapture();
506
+ clearAutoCapture();
507
+ clearRecordingInterval();
508
+ },
509
+ [
510
+ clearVideoCheckInterval,
511
+ stopAutoCapture,
512
+ clearAutoCapture,
513
+ clearRecordingInterval,
514
+ ]
515
+ );
516
+
517
+ useEffect(() => {
518
+ if (
519
+ autoCapture?.startOnReady &&
520
+ isWebcamReady &&
521
+ !isAutoCapturing &&
522
+ !hasAutoStarted
523
+ ) {
524
+ startAutoCapture(false); // Automatic start
525
+ }
526
+ }, [
527
+ autoCapture,
528
+ isWebcamReady,
529
+ isAutoCapturing,
530
+ hasAutoStarted,
531
+ startAutoCapture,
532
+ ]);
533
+
534
+ // Render functions
535
+ const renderVideoRecordingControls = () => {
536
+ if (!videoRecording?.hasInterface || forceHideInterface) return null;
537
+
538
+ return (
539
+ <div
540
+ className="flex flex-col items-center space-y-2"
541
+ key="video-recording-controls"
542
+ >
543
+ {isRecording ? (
544
+ <>
545
+ <Button
546
+ className="size-12 rounded-full border-2 border-red-500 bg-red-500 p-0 text-white"
547
+ onClick={stopVideoRecording}
548
+ >
549
+ <Square className="size-4" />
550
+ </Button>
551
+ <div className="flex items-center space-x-2 text-white">
552
+ <div className="h-2 w-2 animate-pulse rounded-full bg-red-500" />
553
+ <span className="text-xs">
554
+ {defaultLabels.recording} {Math.floor(recordingDuration / 60)}:
555
+ {String(recordingDuration % 60).padStart(2, "0")}
556
+ </span>
557
+ </div>
558
+ </>
559
+ ) : (
560
+ <Button
561
+ className="size-12 rounded-full border-2 border-white bg-white/10 p-0 text-white"
562
+ disabled={!isWebcamReady}
563
+ onClick={startVideoRecording}
564
+ >
565
+ <Video className="size-4" />
566
+ </Button>
567
+ )}
568
+ </div>
569
+ );
570
+ };
571
+
572
+ const renderPhotoCaptureControls = () => {
573
+ if (!photoCapture?.hasInterface || forceHideInterface) return null;
574
+
575
+ return (
576
+ <Button
577
+ key="photo-capture-controls"
578
+ className="size-12 rounded-full border-2 border-white bg-white/10 p-0 text-white transition-all hover:bg-white hover:ring-4"
579
+ disabled={isPending || !isWebcamReady}
580
+ onClick={handleCapturePhoto}
581
+ >
582
+ <Camera className="size-4" />
583
+ <span className="sr-only">{defaultLabels.capturePhoto}</span>
584
+ </Button>
585
+ );
586
+ };
587
+
588
+ const renderAutoCaptureControls = () => {
589
+ if (!autoCapture?.hasInterface || forceHideInterface) return null;
590
+
591
+ return (
592
+ <div
593
+ className="flex flex-col items-center space-y-2"
594
+ key="auto-capture-controls"
595
+ >
596
+ {isAutoCapturing ? (
597
+ <>
598
+ <Button
599
+ className="size-12 rounded-full border-2 p-0 text-white"
600
+ variant="ghost"
601
+ onClick={stopAutoCapture}
602
+ >
603
+ <Pause className="size-4" />
604
+ </Button>
605
+ <div className="flex items-center space-x-2 text-white">
606
+ <div className="h-2 w-2 animate-pulse rounded-full bg-green-500" />
607
+ <span className="text-xs">{defaultLabels.autoCapturing}</span>
608
+ </div>
609
+ </>
610
+ ) : (
611
+ <Button
612
+ className="size-12 rounded-full border-2 p-0 text-white"
613
+ variant="ghost"
614
+ disabled={!isWebcamReady}
615
+ onClick={handleStartAutoCapture}
616
+ >
617
+ <Play className="size-4" />
618
+ <span className="sr-only">{defaultLabels.startAutoCapture}</span>
619
+ </Button>
620
+ )}
621
+ </div>
622
+ );
623
+ };
624
+
625
+ const renderCameraSwitchButton = () => {
626
+ if (!allowCameraSwitch || forceHideInterface) return null;
627
+
628
+ return (
629
+ <Button
630
+ key="camera-switch-button"
631
+ className="size-8 rounded-full bg-white/10 p-0 text-white"
632
+ onClick={switchCamera}
633
+ variant="ghost"
634
+ >
635
+ <RefreshCw className="size-4" />
636
+ <span className="sr-only">{defaultLabels.switchCamera}</span>
637
+ </Button>
638
+ );
639
+ };
640
+
641
+ const renderCapturedImagePreview = () => {
642
+ if (!showCapturedImage || !capturedImage) return null;
643
+
644
+ return (
645
+ <div className={cn("captured size-10", classNames?.previewImage)}>
646
+ <img
647
+ src={capturedImage}
648
+ alt="Captured"
649
+ className="size-10 rounded-md object-cover"
650
+ />
651
+ </div>
652
+ );
653
+ };
654
+
655
+ const renderControls = () => {
656
+ const controls = [];
657
+
658
+ if (videoRecording?.hasInterface || forceHideInterface) {
659
+ controls.push(renderVideoRecordingControls());
660
+ }
661
+
662
+ if (photoCapture?.hasInterface || forceHideInterface) {
663
+ controls.push(renderPhotoCaptureControls());
664
+ }
665
+
666
+ if (autoCapture?.hasInterface || forceHideInterface) {
667
+ controls.push(renderAutoCaptureControls());
668
+ }
669
+
670
+ return controls.filter(Boolean);
671
+ };
672
+
673
+ return (
674
+ <div
675
+ className={cn(
676
+ "webcam-container overflow-hidden rounded-md bg-black relative",
677
+ classNames?.container
678
+ )}
679
+ >
680
+ <div
681
+ className={cn(
682
+ "webcam relative",
683
+ showBorder && "p-2",
684
+ classNames?.webcam
685
+ )}
686
+ >
687
+ <div className="relative">
688
+ <WebcamCore
689
+ audio={false}
690
+ className="background-transparent h-auto w-full rounded-md"
691
+ mirrored={facingMode === "user"}
692
+ onUserMedia={handleUserMedia}
693
+ onUserMediaError={handleUserMediaError}
694
+ ref={webcamRef}
695
+ screenshotFormat="image/jpeg"
696
+ screenshotQuality={1}
697
+ videoConstraints={{
698
+ facingMode,
699
+ width: { ideal: 1920, min: 640 },
700
+ height: { ideal: 1920, min: 640 },
701
+ frameRate: { ideal: 30, min: 15 },
702
+ aspectRatio: isMobile ? 1.0 : 4 / 3,
703
+ }}
704
+ />
705
+
706
+ {placeholder && isWebcamReady && (
707
+ <div
708
+ className={cn(
709
+ "absolute w-full h-full inset-0 z-3",
710
+ classNames?.placeholder
711
+ )}
712
+ >
713
+ {placeholder}
714
+ </div>
715
+ )}
716
+ </div>
717
+ </div>
718
+
719
+ <div
720
+ className={cn(
721
+ "actions flex items-center justify-between p-2 pt-0",
722
+ classNames?.controls,
723
+ interfaceLocation === "absolute"
724
+ ? "absolute bottom-0 left-0 p-4 z-10"
725
+ : ""
726
+ )}
727
+ >
728
+ {renderCapturedImagePreview()}
729
+
730
+ <div className="flex items-center space-x-4">
731
+ {renderControls()}
732
+ {renderCameraSwitchButton()}
733
+ </div>
734
+ </div>
735
+ </div>
736
+ );
737
+ }