markupr 2.1.8

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 (299) hide show
  1. package/.claude/commands/review-feedback.md +47 -0
  2. package/.eslintrc.json +35 -0
  3. package/.github/CODEOWNERS +16 -0
  4. package/.github/FUNDING.yml +1 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
  8. package/.github/dependabot.yml +70 -0
  9. package/.github/workflows/ci.yml +184 -0
  10. package/.github/workflows/deploy-landing.yml +134 -0
  11. package/.github/workflows/nightly.yml +288 -0
  12. package/.github/workflows/release.yml +318 -0
  13. package/CHANGELOG.md +127 -0
  14. package/CLAUDE.md +137 -0
  15. package/CODE_OF_CONDUCT.md +9 -0
  16. package/CONTRIBUTING.md +390 -0
  17. package/LICENSE +21 -0
  18. package/PRODUCT_VISION.md +277 -0
  19. package/README.md +517 -0
  20. package/SECURITY.md +51 -0
  21. package/SIGNING_INSTRUCTIONS.md +284 -0
  22. package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
  23. package/assets/svg-source/dmg-background.svg +70 -0
  24. package/assets/svg-source/icon.svg +20 -0
  25. package/assets/svg-source/tray-icon-processing.svg +7 -0
  26. package/assets/svg-source/tray-icon-recording.svg +7 -0
  27. package/assets/svg-source/tray-icon.svg +6 -0
  28. package/assets/tray-complete.png +0 -0
  29. package/assets/tray-complete@2x.png +0 -0
  30. package/assets/tray-completeTemplate.png +0 -0
  31. package/assets/tray-completeTemplate@2x.png +0 -0
  32. package/assets/tray-error.png +0 -0
  33. package/assets/tray-error@2x.png +0 -0
  34. package/assets/tray-errorTemplate.png +0 -0
  35. package/assets/tray-errorTemplate@2x.png +0 -0
  36. package/assets/tray-icon-processing.png +0 -0
  37. package/assets/tray-icon-processing@2x.png +0 -0
  38. package/assets/tray-icon-processingTemplate.png +0 -0
  39. package/assets/tray-icon-processingTemplate@2x.png +0 -0
  40. package/assets/tray-icon-recording.png +0 -0
  41. package/assets/tray-icon-recording@2x.png +0 -0
  42. package/assets/tray-icon-recordingTemplate.png +0 -0
  43. package/assets/tray-icon-recordingTemplate@2x.png +0 -0
  44. package/assets/tray-icon.png +0 -0
  45. package/assets/tray-icon@2x.png +0 -0
  46. package/assets/tray-iconTemplate.png +0 -0
  47. package/assets/tray-iconTemplate@2x.png +0 -0
  48. package/assets/tray-idle.png +0 -0
  49. package/assets/tray-idle@2x.png +0 -0
  50. package/assets/tray-idleTemplate.png +0 -0
  51. package/assets/tray-idleTemplate@2x.png +0 -0
  52. package/assets/tray-processing-0.png +0 -0
  53. package/assets/tray-processing-0@2x.png +0 -0
  54. package/assets/tray-processing-0Template.png +0 -0
  55. package/assets/tray-processing-0Template@2x.png +0 -0
  56. package/assets/tray-processing-1.png +0 -0
  57. package/assets/tray-processing-1@2x.png +0 -0
  58. package/assets/tray-processing-1Template.png +0 -0
  59. package/assets/tray-processing-1Template@2x.png +0 -0
  60. package/assets/tray-processing-2.png +0 -0
  61. package/assets/tray-processing-2@2x.png +0 -0
  62. package/assets/tray-processing-2Template.png +0 -0
  63. package/assets/tray-processing-2Template@2x.png +0 -0
  64. package/assets/tray-processing-3.png +0 -0
  65. package/assets/tray-processing-3@2x.png +0 -0
  66. package/assets/tray-processing-3Template.png +0 -0
  67. package/assets/tray-processing-3Template@2x.png +0 -0
  68. package/assets/tray-processing.png +0 -0
  69. package/assets/tray-processing@2x.png +0 -0
  70. package/assets/tray-processingTemplate.png +0 -0
  71. package/assets/tray-processingTemplate@2x.png +0 -0
  72. package/assets/tray-recording.png +0 -0
  73. package/assets/tray-recording@2x.png +0 -0
  74. package/assets/tray-recordingTemplate.png +0 -0
  75. package/assets/tray-recordingTemplate@2x.png +0 -0
  76. package/build/DMG_BACKGROUND_SPEC.md +50 -0
  77. package/build/dmg-background.png +0 -0
  78. package/build/dmg-background@2x.png +0 -0
  79. package/build/entitlements.mac.inherit.plist +27 -0
  80. package/build/entitlements.mac.plist +41 -0
  81. package/build/favicon-16.png +0 -0
  82. package/build/favicon-180.png +0 -0
  83. package/build/favicon-192.png +0 -0
  84. package/build/favicon-32.png +0 -0
  85. package/build/favicon-48.png +0 -0
  86. package/build/favicon-512.png +0 -0
  87. package/build/favicon-64.png +0 -0
  88. package/build/icon-128.png +0 -0
  89. package/build/icon-16.png +0 -0
  90. package/build/icon-24.png +0 -0
  91. package/build/icon-256.png +0 -0
  92. package/build/icon-32.png +0 -0
  93. package/build/icon-48.png +0 -0
  94. package/build/icon-64.png +0 -0
  95. package/build/icon.icns +0 -0
  96. package/build/icon.ico +0 -0
  97. package/build/icon.iconset/icon_128x128.png +0 -0
  98. package/build/icon.iconset/icon_128x128@2x.png +0 -0
  99. package/build/icon.iconset/icon_16x16.png +0 -0
  100. package/build/icon.iconset/icon_16x16@2x.png +0 -0
  101. package/build/icon.iconset/icon_256x256.png +0 -0
  102. package/build/icon.iconset/icon_256x256@2x.png +0 -0
  103. package/build/icon.iconset/icon_32x32.png +0 -0
  104. package/build/icon.iconset/icon_32x32@2x.png +0 -0
  105. package/build/icon.iconset/icon_512x512.png +0 -0
  106. package/build/icon.iconset/icon_512x512@2x.png +0 -0
  107. package/build/icon.png +0 -0
  108. package/build/installer-header.bmp +0 -0
  109. package/build/installer-header.png +0 -0
  110. package/build/installer-sidebar.bmp +0 -0
  111. package/build/installer-sidebar.png +0 -0
  112. package/build/installer.nsh +45 -0
  113. package/build/overlay-processing.png +0 -0
  114. package/build/overlay-recording.png +0 -0
  115. package/build/toolbar-record.png +0 -0
  116. package/build/toolbar-screenshot.png +0 -0
  117. package/build/toolbar-settings.png +0 -0
  118. package/build/toolbar-stop.png +0 -0
  119. package/dist/main/index.mjs +12612 -0
  120. package/dist/preload/index.mjs +907 -0
  121. package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
  122. package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
  123. package/dist/renderer/index.html +27 -0
  124. package/docs/AI_AGENT_QUICKSTART.md +42 -0
  125. package/docs/AI_PIPELINE_DESIGN.md +595 -0
  126. package/docs/API.md +514 -0
  127. package/docs/ARCHITECTURE.md +460 -0
  128. package/docs/CONFIGURATION.md +336 -0
  129. package/docs/DEVELOPMENT.md +508 -0
  130. package/docs/EXPORT_FORMATS.md +451 -0
  131. package/docs/GETTING_STARTED.md +236 -0
  132. package/docs/KEYBOARD_SHORTCUTS.md +334 -0
  133. package/docs/TROUBLESHOOTING.md +418 -0
  134. package/docs/landing/index.html +672 -0
  135. package/docs/landing/script.js +342 -0
  136. package/docs/landing/styles.css +1543 -0
  137. package/electron-builder.yml +140 -0
  138. package/electron.vite.config.ts +63 -0
  139. package/package.json +108 -0
  140. package/railway.json +12 -0
  141. package/scripts/build.mjs +51 -0
  142. package/scripts/generate-icons.mjs +314 -0
  143. package/scripts/generate-installer-images.cjs +253 -0
  144. package/scripts/generate-tray-icons.mjs +258 -0
  145. package/scripts/notarize.cjs +180 -0
  146. package/scripts/one-click-clean-test.sh +147 -0
  147. package/scripts/postinstall.mjs +36 -0
  148. package/scripts/setup-markupr.sh +55 -0
  149. package/setup +17 -0
  150. package/site/index.html +1835 -0
  151. package/site/package.json +11 -0
  152. package/site/railway.json +12 -0
  153. package/site/server.js +31 -0
  154. package/src/main/AutoUpdater.ts +392 -0
  155. package/src/main/CrashRecovery.ts +655 -0
  156. package/src/main/ErrorHandler.ts +703 -0
  157. package/src/main/HotkeyManager.ts +399 -0
  158. package/src/main/MenuManager.ts +529 -0
  159. package/src/main/PermissionManager.ts +420 -0
  160. package/src/main/SessionController.ts +1465 -0
  161. package/src/main/TrayManager.ts +540 -0
  162. package/src/main/ai/AIPipelineManager.ts +199 -0
  163. package/src/main/ai/ClaudeAnalyzer.ts +339 -0
  164. package/src/main/ai/ImageOptimizer.ts +176 -0
  165. package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
  166. package/src/main/ai/index.ts +16 -0
  167. package/src/main/ai/types.ts +258 -0
  168. package/src/main/analysis/ClarificationGenerator.ts +385 -0
  169. package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
  170. package/src/main/analysis/index.ts +19 -0
  171. package/src/main/audio/AudioCapture.ts +978 -0
  172. package/src/main/audio/audioUtils.ts +100 -0
  173. package/src/main/audio/index.ts +20 -0
  174. package/src/main/capture/index.ts +1 -0
  175. package/src/main/index.ts +1693 -0
  176. package/src/main/ipc/captureHandlers.ts +272 -0
  177. package/src/main/ipc/index.ts +45 -0
  178. package/src/main/ipc/outputHandlers.ts +302 -0
  179. package/src/main/ipc/sessionHandlers.ts +56 -0
  180. package/src/main/ipc/settingsHandlers.ts +471 -0
  181. package/src/main/ipc/types.ts +56 -0
  182. package/src/main/ipc/windowHandlers.ts +277 -0
  183. package/src/main/output/ClipboardService.ts +369 -0
  184. package/src/main/output/ExportService.ts +539 -0
  185. package/src/main/output/FileManager.ts +416 -0
  186. package/src/main/output/MarkdownGenerator.ts +791 -0
  187. package/src/main/output/MarkdownPatcher.ts +299 -0
  188. package/src/main/output/index.ts +186 -0
  189. package/src/main/output/sessionAdapter.ts +207 -0
  190. package/src/main/output/templates/html-template.ts +553 -0
  191. package/src/main/pipeline/FrameExtractor.ts +330 -0
  192. package/src/main/pipeline/PostProcessor.ts +399 -0
  193. package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
  194. package/src/main/pipeline/index.ts +36 -0
  195. package/src/main/platform/WindowsTaskbar.ts +600 -0
  196. package/src/main/platform/index.ts +16 -0
  197. package/src/main/settings/SettingsManager.ts +730 -0
  198. package/src/main/settings/index.ts +19 -0
  199. package/src/main/transcription/ModelDownloadManager.ts +494 -0
  200. package/src/main/transcription/TierManager.ts +219 -0
  201. package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
  202. package/src/main/transcription/WhisperService.ts +748 -0
  203. package/src/main/transcription/index.ts +56 -0
  204. package/src/main/transcription/types.ts +135 -0
  205. package/src/main/windows/PopoverManager.ts +284 -0
  206. package/src/main/windows/TaskbarIntegration.ts +452 -0
  207. package/src/main/windows/index.ts +23 -0
  208. package/src/preload/index.ts +1047 -0
  209. package/src/renderer/App.tsx +515 -0
  210. package/src/renderer/AppWrapper.tsx +28 -0
  211. package/src/renderer/assets/logo-dark.svg +7 -0
  212. package/src/renderer/assets/logo.svg +7 -0
  213. package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
  214. package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
  215. package/src/renderer/components/AnnotationOverlay.tsx +836 -0
  216. package/src/renderer/components/AudioWaveform.tsx +811 -0
  217. package/src/renderer/components/ClarificationQuestions.tsx +656 -0
  218. package/src/renderer/components/CountdownTimer.tsx +495 -0
  219. package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
  220. package/src/renderer/components/DonateButton.tsx +127 -0
  221. package/src/renderer/components/ErrorBoundary.tsx +308 -0
  222. package/src/renderer/components/ExportDialog.tsx +872 -0
  223. package/src/renderer/components/HotkeyHint.tsx +261 -0
  224. package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
  225. package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
  226. package/src/renderer/components/Onboarding.tsx +1830 -0
  227. package/src/renderer/components/ProcessingOverlay.tsx +157 -0
  228. package/src/renderer/components/RecordingOverlay.tsx +423 -0
  229. package/src/renderer/components/SessionHistory.tsx +1746 -0
  230. package/src/renderer/components/SessionReview.tsx +1321 -0
  231. package/src/renderer/components/SettingsPanel.tsx +217 -0
  232. package/src/renderer/components/Skeleton.tsx +347 -0
  233. package/src/renderer/components/StatusIndicator.tsx +86 -0
  234. package/src/renderer/components/ThemeProvider.tsx +429 -0
  235. package/src/renderer/components/Tooltip.tsx +370 -0
  236. package/src/renderer/components/TranscriptionPreview.tsx +183 -0
  237. package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
  238. package/src/renderer/components/UpdateNotification.tsx +377 -0
  239. package/src/renderer/components/WindowSelector.tsx +947 -0
  240. package/src/renderer/components/index.ts +99 -0
  241. package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
  242. package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
  243. package/src/renderer/components/primitives/DangerButton.tsx +45 -0
  244. package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
  245. package/src/renderer/components/primitives/Dropdown.tsx +34 -0
  246. package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
  247. package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
  248. package/src/renderer/components/primitives/Slider.tsx +43 -0
  249. package/src/renderer/components/primitives/Toggle.tsx +36 -0
  250. package/src/renderer/components/primitives/index.ts +10 -0
  251. package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
  252. package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
  253. package/src/renderer/components/settings/GeneralTab.tsx +40 -0
  254. package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
  255. package/src/renderer/components/settings/RecordingTab.tsx +84 -0
  256. package/src/renderer/components/settings/index.ts +9 -0
  257. package/src/renderer/components/settings/settingsStyles.ts +673 -0
  258. package/src/renderer/components/settings/tabConfig.tsx +85 -0
  259. package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
  260. package/src/renderer/contexts/ProcessingContext.tsx +227 -0
  261. package/src/renderer/contexts/RecordingContext.tsx +683 -0
  262. package/src/renderer/contexts/UIContext.tsx +326 -0
  263. package/src/renderer/contexts/index.ts +24 -0
  264. package/src/renderer/donateMessages.ts +69 -0
  265. package/src/renderer/hooks/index.ts +75 -0
  266. package/src/renderer/hooks/useAnimation.tsx +544 -0
  267. package/src/renderer/hooks/useTheme.ts +313 -0
  268. package/src/renderer/index.html +26 -0
  269. package/src/renderer/main.tsx +52 -0
  270. package/src/renderer/styles/animations.css +1093 -0
  271. package/src/renderer/styles/app-shell.css +662 -0
  272. package/src/renderer/styles/globals.css +515 -0
  273. package/src/renderer/styles/theme.ts +578 -0
  274. package/src/renderer/types/electron.d.ts +385 -0
  275. package/src/shared/hotkeys.ts +283 -0
  276. package/src/shared/types.ts +809 -0
  277. package/tests/clipboard.test.ts +228 -0
  278. package/tests/e2e/criticalPaths.test.ts +594 -0
  279. package/tests/feedbackAnalyzer.test.ts +303 -0
  280. package/tests/integration/sessionFlow.test.ts +583 -0
  281. package/tests/markdownGenerator.test.ts +418 -0
  282. package/tests/output.test.ts +96 -0
  283. package/tests/setup.ts +486 -0
  284. package/tests/unit/appIntegration.test.ts +676 -0
  285. package/tests/unit/appViewState.test.ts +281 -0
  286. package/tests/unit/audioIpcChannels.test.ts +17 -0
  287. package/tests/unit/exportService.test.ts +492 -0
  288. package/tests/unit/hotkeys.test.ts +92 -0
  289. package/tests/unit/navigationPreload.test.ts +94 -0
  290. package/tests/unit/onboardingFlow.test.ts +345 -0
  291. package/tests/unit/permissionManager.test.ts +175 -0
  292. package/tests/unit/permissionManagerExpanded.test.ts +296 -0
  293. package/tests/unit/screenRecordingRenderer.test.ts +368 -0
  294. package/tests/unit/sessionController.test.ts +515 -0
  295. package/tests/unit/tierManager.test.ts +61 -0
  296. package/tests/unit/tierManagerExpanded.test.ts +142 -0
  297. package/tests/unit/transcriptAnalyzer.test.ts +64 -0
  298. package/tsconfig.json +25 -0
  299. package/vitest.config.ts +46 -0
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Countdown Timer Component
3
+ *
4
+ * Cinematic 3-2-1 countdown before recording starts.
5
+ * Features:
6
+ * - Large animated numbers with scale/fade transitions
7
+ * - Optional subtle tick sounds via Web Audio API
8
+ * - Skip button (Escape or Space to skip)
9
+ * - Configurable duration (0, 3, or 5 seconds)
10
+ *
11
+ * Design: Full-screen overlay with dramatic, focused presentation
12
+ */
13
+
14
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
15
+ import { useTheme } from '../hooks/useTheme';
16
+ import { useReducedMotion } from '../hooks/useAnimation';
17
+
18
+ export interface CountdownTimerProps {
19
+ /** Countdown duration in seconds (0 skips countdown entirely) */
20
+ duration: 0 | 3 | 5;
21
+ /** Called when countdown completes or is skipped */
22
+ onComplete: () => void;
23
+ /** Called when user explicitly skips the countdown */
24
+ onSkip: () => void;
25
+ /** Enable subtle tick sounds on each count */
26
+ enableSound?: boolean;
27
+ /** Theme mode */
28
+ isDarkMode?: boolean;
29
+ }
30
+
31
+ type CountdownPhase = 'counting' | 'go' | 'done';
32
+
33
+ // Audio context singleton to avoid multiple instances
34
+ let audioContext: AudioContext | null = null;
35
+
36
+ function getAudioContext(): AudioContext | null {
37
+ if (typeof window === 'undefined') return null;
38
+
39
+ if (!audioContext) {
40
+ const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
41
+ if (AudioContextClass) {
42
+ audioContext = new AudioContextClass();
43
+ }
44
+ }
45
+
46
+ return audioContext;
47
+ }
48
+
49
+ /**
50
+ * Play a subtle tick sound for countdown numbers
51
+ */
52
+ function playTickSound(): void {
53
+ const ctx = getAudioContext();
54
+ if (!ctx) return;
55
+
56
+ // Resume context if suspended (browser autoplay policy)
57
+ if (ctx.state === 'suspended') {
58
+ ctx.resume();
59
+ }
60
+
61
+ const oscillator = ctx.createOscillator();
62
+ const gainNode = ctx.createGain();
63
+
64
+ oscillator.connect(gainNode);
65
+ gainNode.connect(ctx.destination);
66
+
67
+ // Crisp, subtle tick
68
+ oscillator.frequency.value = 880; // A5
69
+ oscillator.type = 'sine';
70
+
71
+ gainNode.gain.setValueAtTime(0.12, ctx.currentTime);
72
+ gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.08);
73
+
74
+ oscillator.start(ctx.currentTime);
75
+ oscillator.stop(ctx.currentTime + 0.08);
76
+ }
77
+
78
+ /**
79
+ * Play a "go" sound when recording starts
80
+ */
81
+ function playGoSound(): void {
82
+ const ctx = getAudioContext();
83
+ if (!ctx) return;
84
+
85
+ if (ctx.state === 'suspended') {
86
+ ctx.resume();
87
+ }
88
+
89
+ const oscillator = ctx.createOscillator();
90
+ const gainNode = ctx.createGain();
91
+
92
+ oscillator.connect(gainNode);
93
+ gainNode.connect(ctx.destination);
94
+
95
+ // Ascending tone for "Recording!"
96
+ oscillator.frequency.setValueAtTime(880, ctx.currentTime); // A5
97
+ oscillator.frequency.exponentialRampToValueAtTime(1318.5, ctx.currentTime + 0.15); // E6
98
+ oscillator.type = 'sine';
99
+
100
+ gainNode.gain.setValueAtTime(0.15, ctx.currentTime);
101
+ gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.25);
102
+
103
+ oscillator.start(ctx.currentTime);
104
+ oscillator.stop(ctx.currentTime + 0.25);
105
+ }
106
+
107
+ export const CountdownTimer: React.FC<CountdownTimerProps> = ({
108
+ duration,
109
+ onComplete,
110
+ onSkip,
111
+ enableSound = false,
112
+ isDarkMode = true,
113
+ }) => {
114
+ const { colors } = useTheme();
115
+ const prefersReducedMotion = useReducedMotion();
116
+ const [count, setCount] = useState<number>(duration);
117
+ const [phase, setPhase] = useState<CountdownPhase>('counting');
118
+ const [isExiting, setIsExiting] = useState(false);
119
+
120
+ const hasPlayedInitialRef = useRef(false);
121
+ const completeCalledRef = useRef(false);
122
+
123
+ // Skip if duration is 0 or user prefers reduced motion
124
+ useEffect(() => {
125
+ if ((duration === 0 || prefersReducedMotion) && !completeCalledRef.current) {
126
+ completeCalledRef.current = true;
127
+ onComplete();
128
+ }
129
+ }, [duration, prefersReducedMotion, onComplete]);
130
+
131
+ // Play initial tick when countdown starts
132
+ useEffect(() => {
133
+ if (duration > 0 && enableSound && !hasPlayedInitialRef.current) {
134
+ hasPlayedInitialRef.current = true;
135
+ playTickSound();
136
+ }
137
+ }, [duration, enableSound]);
138
+
139
+ // Countdown logic
140
+ useEffect(() => {
141
+ if (duration === 0 || phase === 'done') return;
142
+
143
+ if (count > 0) {
144
+ const timer = setTimeout(() => {
145
+ setCount((c) => {
146
+ const newCount = c - 1;
147
+ // Play tick for next number
148
+ if (newCount > 0 && enableSound) {
149
+ playTickSound();
150
+ }
151
+ return newCount;
152
+ });
153
+ }, 1000);
154
+
155
+ return () => clearTimeout(timer);
156
+ } else if (phase === 'counting') {
157
+ // Transition to "Recording!" message
158
+ setPhase('go');
159
+ if (enableSound) {
160
+ playGoSound();
161
+ }
162
+
163
+ // Brief "Recording!" display, then complete
164
+ const goTimer = setTimeout(() => {
165
+ setIsExiting(true);
166
+ setTimeout(() => {
167
+ setPhase('done');
168
+ if (!completeCalledRef.current) {
169
+ completeCalledRef.current = true;
170
+ onComplete();
171
+ }
172
+ }, 300); // Exit animation duration
173
+ }, 700);
174
+
175
+ return () => clearTimeout(goTimer);
176
+ }
177
+ }, [count, phase, duration, enableSound, onComplete]);
178
+
179
+ // Handle skip action
180
+ const handleSkip = useCallback(() => {
181
+ if (phase === 'done') return;
182
+
183
+ setIsExiting(true);
184
+ setTimeout(() => {
185
+ setPhase('done');
186
+ if (!completeCalledRef.current) {
187
+ completeCalledRef.current = true;
188
+ onSkip();
189
+ }
190
+ }, 150);
191
+ }, [phase, onSkip]);
192
+
193
+ // Keyboard support (Escape or Space to skip)
194
+ useEffect(() => {
195
+ const handleKeyDown = (e: KeyboardEvent): void => {
196
+ if (e.key === 'Escape' || e.key === ' ') {
197
+ e.preventDefault();
198
+ handleSkip();
199
+ }
200
+ };
201
+
202
+ window.addEventListener('keydown', handleKeyDown);
203
+ return () => window.removeEventListener('keydown', handleKeyDown);
204
+ }, [handleSkip]);
205
+
206
+ // Don't render if duration is 0, reduced motion preferred, or phase is done
207
+ if (duration === 0 || prefersReducedMotion || phase === 'done') return null;
208
+
209
+ // Theme colors
210
+ const theme = {
211
+ overlayBg: isDarkMode ? 'rgba(0, 0, 0, 0.85)' : 'rgba(0, 0, 0, 0.75)',
212
+ numberColor: colors.text.inverse,
213
+ numberGlow: isDarkMode ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 0.5)',
214
+ recordingColor: colors.status.error,
215
+ recordingGlow: 'rgba(239, 68, 68, 0.5)',
216
+ skipText: isDarkMode ? 'rgba(156, 163, 175, 0.8)' : 'rgba(156, 163, 175, 0.9)',
217
+ skipTextHover: isDarkMode ? 'rgba(209, 213, 219, 1)' : 'rgba(255, 255, 255, 1)',
218
+ };
219
+
220
+ return (
221
+ <>
222
+ {/* Keyframe animations */}
223
+ <style>
224
+ {`
225
+ @keyframes countdown-number-enter {
226
+ 0% {
227
+ opacity: 0;
228
+ transform: scale(0.3);
229
+ }
230
+ 30% {
231
+ opacity: 1;
232
+ transform: scale(1.15);
233
+ }
234
+ 50% {
235
+ transform: scale(0.95);
236
+ }
237
+ 70% {
238
+ transform: scale(1.02);
239
+ }
240
+ 100% {
241
+ transform: scale(1);
242
+ }
243
+ }
244
+
245
+ @keyframes countdown-number-pulse {
246
+ 0%, 100% {
247
+ transform: scale(1);
248
+ text-shadow: 0 0 60px ${theme.numberGlow};
249
+ }
250
+ 50% {
251
+ transform: scale(1.02);
252
+ text-shadow: 0 0 80px ${theme.numberGlow}, 0 0 120px ${theme.numberGlow};
253
+ }
254
+ }
255
+
256
+ @keyframes countdown-number-exit {
257
+ 0% {
258
+ opacity: 1;
259
+ transform: scale(1);
260
+ }
261
+ 100% {
262
+ opacity: 0;
263
+ transform: scale(1.8);
264
+ }
265
+ }
266
+
267
+ @keyframes countdown-recording-pulse {
268
+ 0%, 100% {
269
+ opacity: 1;
270
+ transform: scale(1);
271
+ }
272
+ 50% {
273
+ opacity: 0.8;
274
+ transform: scale(1.1);
275
+ }
276
+ }
277
+
278
+ @keyframes countdown-dot-ping {
279
+ 0% {
280
+ transform: scale(1);
281
+ opacity: 0.8;
282
+ }
283
+ 75%, 100% {
284
+ transform: scale(2.5);
285
+ opacity: 0;
286
+ }
287
+ }
288
+
289
+ @keyframes countdown-overlay-enter {
290
+ 0% {
291
+ opacity: 0;
292
+ backdrop-filter: blur(0px);
293
+ }
294
+ 100% {
295
+ opacity: 1;
296
+ backdrop-filter: blur(12px);
297
+ }
298
+ }
299
+
300
+ @keyframes countdown-overlay-exit {
301
+ 0% {
302
+ opacity: 1;
303
+ }
304
+ 100% {
305
+ opacity: 0;
306
+ }
307
+ }
308
+
309
+ @keyframes countdown-recording-enter {
310
+ 0% {
311
+ opacity: 0;
312
+ transform: scale(0.8);
313
+ }
314
+ 100% {
315
+ opacity: 1;
316
+ transform: scale(1);
317
+ }
318
+ }
319
+
320
+ .countdown-number {
321
+ animation: countdown-number-enter 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards,
322
+ countdown-number-pulse 1s ease-in-out 0.4s infinite;
323
+ }
324
+
325
+ .countdown-number-exiting {
326
+ animation: countdown-number-exit 0.25s ease-out forwards;
327
+ }
328
+ `}
329
+ </style>
330
+
331
+ {/* Full-screen overlay */}
332
+ <div
333
+ style={{
334
+ position: 'fixed',
335
+ inset: 0,
336
+ backgroundColor: theme.overlayBg,
337
+ backdropFilter: 'blur(12px)',
338
+ WebkitBackdropFilter: 'blur(12px)',
339
+ display: 'flex',
340
+ flexDirection: 'column',
341
+ alignItems: 'center',
342
+ justifyContent: 'center',
343
+ zIndex: 99999,
344
+ animation: isExiting
345
+ ? 'countdown-overlay-exit 0.3s ease-out forwards'
346
+ : 'countdown-overlay-enter 0.3s ease-out forwards',
347
+ }}
348
+ >
349
+ <div
350
+ style={{
351
+ textAlign: 'center',
352
+ display: 'flex',
353
+ flexDirection: 'column',
354
+ alignItems: 'center',
355
+ gap: 24,
356
+ }}
357
+ >
358
+ {/* Screen reader announcement */}
359
+ <div aria-live="assertive" aria-atomic="true" style={{ position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0,0,0,0)' }}>
360
+ {phase === 'counting' ? `${count}` : phase === 'go' ? 'Recording started' : ''}
361
+ </div>
362
+
363
+ {/* Countdown number */}
364
+ {phase === 'counting' && (
365
+ <div
366
+ key={count}
367
+ className="countdown-number"
368
+ aria-hidden="true"
369
+ style={{
370
+ fontSize: 'min(40vw, 280px)',
371
+ fontWeight: 800,
372
+ color: theme.numberColor,
373
+ lineHeight: 1,
374
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
375
+ textShadow: `0 0 60px ${theme.numberGlow}`,
376
+ userSelect: 'none',
377
+ }}
378
+ >
379
+ {count}
380
+ </div>
381
+ )}
382
+
383
+ {/* "Recording!" message */}
384
+ {phase === 'go' && (
385
+ <div
386
+ style={{
387
+ display: 'flex',
388
+ flexDirection: 'column',
389
+ alignItems: 'center',
390
+ gap: 16,
391
+ animation: 'countdown-recording-enter 0.3s ease-out forwards',
392
+ }}
393
+ >
394
+ {/* Pulsing recording dot */}
395
+ <div
396
+ style={{
397
+ position: 'relative',
398
+ width: 24,
399
+ height: 24,
400
+ }}
401
+ >
402
+ {/* Ping effect */}
403
+ <div
404
+ style={{
405
+ position: 'absolute',
406
+ inset: 0,
407
+ backgroundColor: theme.recordingColor,
408
+ borderRadius: '50%',
409
+ animation: 'countdown-dot-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
410
+ }}
411
+ />
412
+ {/* Solid dot */}
413
+ <div
414
+ style={{
415
+ position: 'relative',
416
+ width: 24,
417
+ height: 24,
418
+ backgroundColor: theme.recordingColor,
419
+ borderRadius: '50%',
420
+ boxShadow: `0 0 20px ${theme.recordingGlow}`,
421
+ animation: 'countdown-recording-pulse 1s ease-in-out infinite',
422
+ }}
423
+ />
424
+ </div>
425
+
426
+ {/* "Recording!" text */}
427
+ <div
428
+ style={{
429
+ fontSize: 'min(10vw, 48px)',
430
+ fontWeight: 700,
431
+ color: theme.recordingColor,
432
+ letterSpacing: '-0.02em',
433
+ textShadow: `0 0 30px ${theme.recordingGlow}`,
434
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
435
+ userSelect: 'none',
436
+ }}
437
+ >
438
+ Recording!
439
+ </div>
440
+ </div>
441
+ )}
442
+
443
+ {/* Skip instructions */}
444
+ {phase === 'counting' && (
445
+ <button
446
+ onClick={handleSkip}
447
+ style={{
448
+ marginTop: 32,
449
+ padding: '8px 16px',
450
+ backgroundColor: 'transparent',
451
+ border: 'none',
452
+ color: theme.skipText,
453
+ fontSize: 14,
454
+ cursor: 'pointer',
455
+ transition: 'color 0.2s ease',
456
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
457
+ outline: 'none',
458
+ }}
459
+ onMouseEnter={(e) => {
460
+ e.currentTarget.style.color = theme.skipTextHover;
461
+ }}
462
+ onMouseLeave={(e) => {
463
+ e.currentTarget.style.color = theme.skipText;
464
+ }}
465
+ onFocus={(e) => {
466
+ e.currentTarget.style.color = theme.skipTextHover;
467
+ }}
468
+ onBlur={(e) => {
469
+ e.currentTarget.style.color = theme.skipText;
470
+ }}
471
+ >
472
+ Press <kbd style={{
473
+ padding: '2px 6px',
474
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
475
+ borderRadius: 4,
476
+ fontFamily: 'inherit',
477
+ fontSize: 12,
478
+ border: '1px solid rgba(255, 255, 255, 0.2)',
479
+ }}>Esc</kbd> or <kbd style={{
480
+ padding: '2px 6px',
481
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
482
+ borderRadius: 4,
483
+ fontFamily: 'inherit',
484
+ fontSize: 12,
485
+ border: '1px solid rgba(255, 255, 255, 0.2)',
486
+ }}>Space</kbd> to skip
487
+ </button>
488
+ )}
489
+ </div>
490
+ </div>
491
+ </>
492
+ );
493
+ };
494
+
495
+ export default CountdownTimer;