@exode-team/react-recorder 1.1.1 → 1.1.3

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.
package/README.md CHANGED
@@ -144,6 +144,7 @@ const Messages = () => {
144
144
  | `src` | `string` | ✅ | — | URL аудиофайла. |
145
145
  | `chainId` | `number` | — | — | ID для последовательного воспроизведения. При окончании автоматически запускает следующий. |
146
146
  | `duration` | `number` | — | — | Длительность в секундах (отображается до загрузки аудио). |
147
+ | `autoPreload` | `boolean` | — | `true` | Автозагрузка аудио при попадании в viewport (IntersectionObserver). |
147
148
  | `waveformData` | `number[]` | — | — | Предварительно вычисленные уровни waveform. |
148
149
  | `preloadedBlob` | `Blob \| null` | — | — | Blob для мгновенного воспроизведения без сетевого запроса. |
149
150
  | `onEnded` | `() => void` | — | — | Колбэк окончания воспроизведения. |
@@ -153,6 +154,7 @@ const Messages = () => {
153
154
  **Ref API (`VoiceMessagePlayerRef`):**
154
155
 
155
156
  - `play()` — программный запуск воспроизведения.
157
+ - `prefetch()` — загрузка аудио без воспроизведения (fetch + decode waveform).
156
158
 
157
159
  #### `VoicePlaybackChain`
158
160
 
@@ -1 +1 @@
1
- {"version":3,"file":"AudioRecorder.styled.d.ts","sourceRoot":"","sources":["../../src/components/AudioRecorder.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,SAAS,KAMrB,CAAC"}
1
+ {"version":3,"file":"AudioRecorder.styled.d.ts","sourceRoot":"","sources":["../../src/components/AudioRecorder.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,SAAS,KAKrB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"RecordingShell.d.ts","sourceRoot":"","sources":["../../../src/components/RecordingShell/RecordingShell.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA4BH,UAAU,KAAK;IACX,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,uBAAuB,EAAE,MAAM,CAAC;IAChC,wBAAwB,EAAE,MAAM,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3C;AAGD,QAAA,MAAM,cAAc,GAAI,OAAO,KAAK,QAyHnC,CAAC;AAGF,OAAO,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"RecordingShell.d.ts","sourceRoot":"","sources":["../../../src/components/RecordingShell/RecordingShell.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA2BH,UAAU,KAAK;IACX,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,uBAAuB,EAAE,MAAM,CAAC;IAChC,wBAAwB,EAAE,MAAM,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3C;AAGD,QAAA,MAAM,cAAc,GAAI,OAAO,KAAK,QA6HnC,CAAC;AAGF,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"RecordingShell.styled.d.ts","sourceRoot":"","sources":["../../../src/components/RecordingShell/RecordingShell.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,OAAO,KAQnB,CAAC;AAEF,eAAO,MAAM,YAAY,KAqBxB,CAAC;AAEF,eAAO,MAAM,KAAK,KAQjB,CAAC;AAEF,eAAO,MAAM,YAAY,KA2CxB,CAAC;AAEF,eAAO,MAAM,QAAQ,KAOpB,CAAC;AAEF,eAAO,MAAM,IAAI,KAMhB,CAAC;AAEF,eAAO,MAAM,GAAG,KAOf,CAAC;AAEF,eAAO,MAAM,KAAK,KAKjB,CAAC;AAEF,eAAO,MAAM,UAAU,KAyBtB,CAAC;AAEF,eAAO,MAAM,WAAW,KAMvB,CAAC;AAEF,eAAO,MAAM,OAAO,KAoBnB,CAAC"}
1
+ {"version":3,"file":"RecordingShell.styled.d.ts","sourceRoot":"","sources":["../../../src/components/RecordingShell/RecordingShell.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,OAAO,KAOnB,CAAC;AAEF,eAAO,MAAM,YAAY,KAqBxB,CAAC;AAEF,eAAO,MAAM,KAAK,KAQjB,CAAC;AAEF,eAAO,MAAM,YAAY,KA2CxB,CAAC;AAEF,eAAO,MAAM,QAAQ,KAOpB,CAAC;AAEF,eAAO,MAAM,IAAI,KAMhB,CAAC;AAEF,eAAO,MAAM,GAAG,KAOf,CAAC;AAEF,eAAO,MAAM,KAAK,KAKjB,CAAC;AAEF,eAAO,MAAM,UAAU,KAyBtB,CAAC;AAEF,eAAO,MAAM,WAAW,KAMvB,CAAC;AAEF,eAAO,MAAM,OAAO,KAoBnB,CAAC"}
@@ -5,6 +5,7 @@
5
5
  */
6
6
  export interface VoiceMessagePlayerRef {
7
7
  play: () => void;
8
+ prefetch: () => void;
8
9
  }
9
10
  declare const VoiceMessagePlayer: any;
10
11
  export { VoiceMessagePlayer };
@@ -1 +1 @@
1
- {"version":3,"file":"VoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceMessagePlayer/VoiceMessagePlayer.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA0BH,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,MAAM,IAAI,CAAC;CACpB;AAcD,QAAA,MAAM,kBAAkB,KAmJtB,CAAC;AAGH,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"VoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceMessagePlayer/VoiceMessagePlayer.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA0BH,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACxB;AAeD,QAAA,MAAM,kBAAkB,KA8MtB,CAAC;AAGH,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -10,7 +10,7 @@ interface PreviewBarsProps {
10
10
  recordingTime: number;
11
11
  previewLevels: number[];
12
12
  timelineLevels: number[];
13
- getVisibleBarsCount: () => number;
13
+ visibleBarsCount: number;
14
14
  }
15
15
  declare const PreviewBars: (props: PreviewBarsProps) => any;
16
16
  export { PreviewBars };
@@ -1 +1 @@
1
- {"version":3,"file":"PreviewBars.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/PreviewBars.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,UAAU,gBAAgB;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,mBAAmB,EAAE,MAAM,MAAM,CAAC;CACrC;AAGD,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,QA0E3C,CAAC;AAGF,OAAO,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"PreviewBars.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/PreviewBars.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,UAAU,gBAAgB;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;CAC5B;AAGD,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,QA0E3C,CAAC;AAGF,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -5,7 +5,7 @@
5
5
  */
6
6
  interface TimelineBarsProps {
7
7
  timelineLevels: number[];
8
- getVisibleBarsCount: () => number;
8
+ visibleBarsCount: number;
9
9
  }
10
10
  declare const TimelineBars: (props: TimelineBarsProps) => any;
11
11
  export { TimelineBars };
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineBars.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/TimelineBars.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,UAAU,iBAAiB;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,mBAAmB,EAAE,MAAM,MAAM,CAAC;CACrC;AAGD,QAAA,MAAM,YAAY,GAAI,OAAO,iBAAiB,QAkC7C,CAAC;AAGF,OAAO,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"TimelineBars.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/TimelineBars.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,UAAU,iBAAiB;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;CAC5B;AAGD,QAAA,MAAM,YAAY,GAAI,OAAO,iBAAiB,QAkC7C,CAAC;AAGF,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"VoiceRecorderInput.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAuBrE,UAAU,KAAK;IACX,QAAQ,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAGD,QAAA,MAAM,kBAAkB,GAAI,OAAO,KAAK,QAsGvC,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"VoiceRecorderInput.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAuBrE,UAAU,KAAK;IACX,QAAQ,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAGD,QAAA,MAAM,kBAAkB,GAAI,OAAO,KAAK,QAmIvC,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"VoiceRecorderInput.styled.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,OAAO,KAOnB,CAAC;AAEF,eAAO,MAAM,YAAY,KAkBxB,CAAC;AAEF,eAAO,MAAM,WAAW,KASvB,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,UAAU,KAKtB,CAAC;AAEF,eAAO,MAAM,UAAU,KA0BtB,CAAC;AAEF,eAAO,MAAM,YAAY,KAUxB,CAAC;AAEF,eAAO,MAAM,KAAK,KAMjB,CAAC;AAEF,eAAO,MAAM,UAAU,KAuBtB,CAAC;AAEF,eAAO,MAAM,YAAY,KAqBxB,CAAC;AAEF,eAAO,MAAM,eAAe,KAqB3B,CAAC;AAEF,eAAO,MAAM,WAAW,KAcvB,CAAC"}
1
+ {"version":3,"file":"VoiceRecorderInput.styled.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.styled.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,OAAO,KAOnB,CAAC;AAEF,eAAO,MAAM,YAAY,KAcxB,CAAC;AAEF,eAAO,MAAM,WAAW,KASvB,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,UAAU,KAMtB,CAAC;AAEF,eAAO,MAAM,UAAU,KA0BtB,CAAC;AAEF,eAAO,MAAM,YAAY,KAUxB,CAAC;AAEF,eAAO,MAAM,KAAK,KAMjB,CAAC;AAEF,eAAO,MAAM,UAAU,KAuBtB,CAAC;AAEF,eAAO,MAAM,YAAY,KAqBxB,CAAC;AAEF,eAAO,MAAM,eAAe,KAqB3B,CAAC;AAEF,eAAO,MAAM,WAAW,KAcvB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useAudioBlobLoader.d.ts","sourceRoot":"","sources":["../../src/hooks/useAudioBlobLoader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAYrD,UAAU,yBAAyB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,iBAAiB,EAAE,iBAAiB,CAAC;CACxC;AAED,UAAU,wBAAwB;IAC9B,QAAQ,EAAE,KAAK,CAAC,gBAAgB,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,QAAA,MAAM,kBAAkB,GAAI,SAAS,yBAAyB,KAAG,wBAgIhE,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"useAudioBlobLoader.d.ts","sourceRoot":"","sources":["../../src/hooks/useAudioBlobLoader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAYrD,UAAU,yBAAyB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,iBAAiB,EAAE,iBAAiB,CAAC;CACxC;AAED,UAAU,wBAAwB;IAC9B,QAAQ,EAAE,KAAK,CAAC,gBAAgB,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,QAAA,MAAM,kBAAkB,GAAI,SAAS,yBAAyB,KAAG,wBAoIhE,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useVoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoiceMessagePlayer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAG1F,QAAA,MAAM,qBAAqB,GAAI,SAAS,4BAA4B,KAAG,2BAmJtE,CAAC;AAGF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
1
+ {"version":3,"file":"useVoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoiceMessagePlayer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAG1F,QAAA,MAAM,qBAAqB,GAAI,SAAS,4BAA4B,KAAG,2BA6JtE,CAAC;AAGF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
package/dist/index.cjs CHANGED
@@ -784,6 +784,7 @@ const useAudioBlobLoader = (options) => {
784
784
  const audioRef = React.useRef(null);
785
785
  const objectUrlRef = React.useRef(null);
786
786
  const abortControllerRef = React.useRef(null);
787
+ const failedSrcRef = React.useRef(null);
787
788
  const callbacksRef = React.useRef(playbackCallbacks);
788
789
  callbacksRef.current = playbackCallbacks;
789
790
  const createAudioFromBlob = React.useCallback((blob) => {
@@ -820,7 +821,7 @@ const useAudioBlobLoader = (options) => {
820
821
  }, [waveformData, createAudioFromBlob]);
821
822
  const ensureAudioLoaded = React.useCallback(async () => {
822
823
  var _a;
823
- if (audioRef.current) {
824
+ if (audioRef.current || failedSrcRef.current === src) {
824
825
  return;
825
826
  }
826
827
  if (preloadedBlob) {
@@ -851,6 +852,7 @@ const useAudioBlobLoader = (options) => {
851
852
  if (error instanceof DOMException && error.name === 'AbortError') {
852
853
  return;
853
854
  }
855
+ failedSrcRef.current = src;
854
856
  console.warn('[voice-player] Failed to load audio', error);
855
857
  }
856
858
  finally {
@@ -861,6 +863,7 @@ const useAudioBlobLoader = (options) => {
861
863
  }, [src, waveformData, preloadedBlob, mountFromBlob, createAudioFromBlob]);
862
864
  /** Cleanup on src change or unmount */
863
865
  React.useEffect(() => {
866
+ failedSrcRef.current = null;
864
867
  return () => {
865
868
  var _a;
866
869
  (_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
@@ -962,6 +965,13 @@ const useVoiceMessagePlayer = (options) => {
962
965
  },
963
966
  },
964
967
  });
968
+ /** Pre-fetch audio without playing (for viewport preloading) */
969
+ const prefetch = React.useCallback(() => {
970
+ if (isLoading || audioRef.current) {
971
+ return;
972
+ }
973
+ void ensureAudioLoaded();
974
+ }, [isLoading, ensureAudioLoaded]);
965
975
  /** Load audio and auto-play after loaded */
966
976
  const preload = React.useCallback(() => {
967
977
  if (isLoading || audioRef.current) {
@@ -1032,6 +1042,7 @@ const useVoiceMessagePlayer = (options) => {
1032
1042
  currentTime,
1033
1043
  waveformLevels,
1034
1044
  seekTo,
1045
+ prefetch,
1035
1046
  preload,
1036
1047
  formatTime,
1037
1048
  togglePlayback,
@@ -1182,7 +1193,6 @@ const useAudioRecorderHandlers = (options) => {
1182
1193
  const Container = styled.div `
1183
1194
  position: relative;
1184
1195
  width: 100%;
1185
- padding: 16px;
1186
1196
  transition: color 200ms ease, background-color 200ms ease;
1187
1197
  color: var(--text_primary, #0a0a0a);
1188
1198
  `;
@@ -1267,29 +1277,28 @@ const Wrapper$1 = styled.div `
1267
1277
  gap: 12px;
1268
1278
  padding: 10px 14px;
1269
1279
  border-radius: 9999px;
1270
- background: var(--background_content, #ffffff);
1271
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent);
1280
+ background: var(--background_hover, color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent));
1272
1281
  `;
1273
1282
  const CancelButton$1 = styled.button `
1274
- width: 36px;
1275
- height: 36px;
1283
+ width: 38px;
1284
+ height: 38px;
1276
1285
  display: inline-flex;
1277
1286
  align-items: center;
1278
1287
  justify-content: center;
1279
- border-radius: 18px;
1280
- background: transparent;
1288
+ border-radius: 19px;
1289
+ background: color-mix(in srgb, var(--exode_red) 10%, transparent);
1281
1290
  border: 0;
1291
+ flex-shrink: 0;
1282
1292
  color: color-mix(in srgb, var(--text_primary, #0a0a0a) 54%, transparent);
1283
1293
  cursor: pointer;
1284
- transition: color 160ms ease, background-color 160ms ease;
1294
+ transition: transform 160ms ease;
1285
1295
 
1286
- &:hover {
1287
- background-color: color-mix(in srgb, var(--text_primary, #0a0a0a) 12%, transparent);
1288
- color: color-mix(in srgb, var(--text_primary, #0a0a0a) 76%, transparent);
1296
+ &:hover:not(:disabled) {
1297
+ transform: translateY(-1px);
1289
1298
  }
1290
1299
 
1291
- &:active {
1292
- background-color: color-mix(in srgb, var(--text_primary, #0a0a0a) 18%, transparent);
1300
+ &:active:not(:disabled) {
1301
+ transform: translateY(0);
1293
1302
  }
1294
1303
  `;
1295
1304
  const Track = styled.div `
@@ -1297,7 +1306,7 @@ const Track = styled.div `
1297
1306
  display: flex;
1298
1307
  align-items: center;
1299
1308
  gap: 12px;
1300
- padding: 8px 12px;
1309
+ padding: 8px 10px;
1301
1310
  border-radius: 9999px;
1302
1311
  background: color-mix(in srgb, var(--text_primary, #0a0a0a) 10%, transparent);
1303
1312
  `;
@@ -1461,7 +1470,9 @@ const RecordingShell = (props) => {
1461
1470
  return formatTime(0);
1462
1471
  }, [formatTime, hasRecording, isPlaying, playbackDuration, playbackTime, recordingTime]);
1463
1472
  const recordButtonDisabled = isSending;
1464
- const recordButtonIcon = !isRecording || isPaused ? jsxRuntime.jsx(icons.Icon16Microphone, {}) : jsxRuntime.jsx(icons.Icon16Square, {});
1473
+ const recordButtonIcon = !isRecording || isPaused
1474
+ ? jsxRuntime.jsx(icons.Icon16Microphone, { width: 24, height: 24 })
1475
+ : jsxRuntime.jsx(icons.Icon16Pause, { width: 24, height: 24 });
1465
1476
  const recordButtonVariant = !isRecording || isPaused ? 'resume' : 'record';
1466
1477
  const recordButtonAria = !isRecording
1467
1478
  ? startAriaLabel
@@ -1470,14 +1481,16 @@ const RecordingShell = (props) => {
1470
1481
  : pauseRecordingAriaLabel || stopAriaLabel;
1471
1482
  const canShowPlayback = !isRecording || isPaused;
1472
1483
  const playbackDisabled = !hasRecording && !isRecording;
1473
- const playbackIcon = isPlaying ? jsxRuntime.jsx(icons.Icon16Pause, {}) : jsxRuntime.jsx(icons.Icon16Play, {});
1484
+ const playbackIcon = isPlaying
1485
+ ? jsxRuntime.jsx(icons.Icon16Pause, { width: 24, height: 24 })
1486
+ : jsxRuntime.jsx(icons.Icon16Play, { width: 24, height: 24 });
1474
1487
  const playbackAria = isPlaying ? pausePlaybackAriaLabel : playAriaLabel;
1475
1488
  const sendDisabled = isSending || (!hasRecording && !isRecording);
1476
- return (jsxRuntime.jsxs(Wrapper$1, { children: [jsxRuntime.jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsxRuntime.jsx(icons.Icon28CancelOutline, { width: 20, height: 20 }) }), jsxRuntime.jsxs(Track, { children: [jsxRuntime.jsx(CircleButton, { type: "button", "$variant": recordButtonVariant, onClick: onRecordToggle, "aria-label": recordButtonAria, disabled: recordButtonDisabled, children: recordButtonIcon }), !canShowPlayback ? null : (jsxRuntime.jsx(CircleButton, { type: "button", "$variant": "play", onClick: onTogglePlayback, "aria-label": playbackAria, disabled: playbackDisabled, children: playbackIcon })), jsxRuntime.jsx(Waveform, { "$expanded": canShowPlayback, children: jsxRuntime.jsx(WaveformBars, { values: waveformValues, divider: 100, scale: 42, minHeight: 6, styleInterpolator: ({ normalized }) => ({
1489
+ return (jsxRuntime.jsxs(Wrapper$1, { children: [jsxRuntime.jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsxRuntime.jsx(icons.Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 24, height: 24, style: { minWidth: 24 } }) }), jsxRuntime.jsxs(Track, { children: [jsxRuntime.jsx(CircleButton, { type: "button", "$variant": recordButtonVariant, onClick: onRecordToggle, "aria-label": recordButtonAria, disabled: recordButtonDisabled, children: recordButtonIcon }), !canShowPlayback ? null : (jsxRuntime.jsx(CircleButton, { type: "button", "$variant": "play", onClick: onTogglePlayback, "aria-label": playbackAria, disabled: playbackDisabled, children: playbackIcon })), jsxRuntime.jsx(Waveform, { "$expanded": canShowPlayback, children: jsxRuntime.jsx(WaveformBars, { values: waveformValues, divider: 100, scale: 42, minHeight: 6, styleInterpolator: ({ normalized }) => ({
1477
1490
  height: `${12 + normalized / 2}px`,
1478
1491
  maxHeight: '34px',
1479
1492
  opacity: 0.38 + Math.min(0.5, normalized / 120),
1480
- }) }) }), jsxRuntime.jsx(Timer$1, { children: displayTime })] }), jsxRuntime.jsx(SendButton$1, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsxRuntime.jsx(SpinnerSlot, { children: jsxRuntime.jsx(Spinner, {}) })) : (jsxRuntime.jsx(icons.Icon28SendOutline, { width: 20, height: 20 })) })] }));
1493
+ }) }) }), jsxRuntime.jsx(Timer$1, { children: displayTime })] }), jsxRuntime.jsx(SendButton$1, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsxRuntime.jsx(SpinnerSlot, { children: jsxRuntime.jsx(Spinner, {}) })) : (jsxRuntime.jsx(icons.Icon28SendOutline, { width: 24, height: 24 })) })] }));
1481
1494
  };
1482
1495
 
1483
1496
  /**
@@ -1737,8 +1750,8 @@ const useVoiceRecorderInput = (options) => {
1737
1750
 
1738
1751
  const PreviewBars = (props) => {
1739
1752
  var _a, _b;
1740
- const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels, getVisibleBarsCount, } = props;
1741
- const maxBars = getVisibleBarsCount();
1753
+ const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels, visibleBarsCount, } = props;
1754
+ const maxBars = visibleBarsCount;
1742
1755
  const sourceLevels = timelineLevels.length > 0
1743
1756
  ? timelineLevels
1744
1757
  : previewLevels;
@@ -1782,8 +1795,8 @@ const PreviewBars = (props) => {
1782
1795
  };
1783
1796
 
1784
1797
  const TimelineBars = (props) => {
1785
- const { timelineLevels, getVisibleBarsCount } = props;
1786
- const maxBars = getVisibleBarsCount();
1798
+ const { timelineLevels, visibleBarsCount } = props;
1799
+ const maxBars = visibleBarsCount;
1787
1800
  const visible = timelineLevels.slice(-maxBars);
1788
1801
  const padded = [
1789
1802
  ...visible,
@@ -1828,15 +1841,11 @@ const CancelButton = styled.button `
1828
1841
  border-radius: 16px;
1829
1842
  background: transparent;
1830
1843
  border: 0;
1844
+ margin-right: 4px;
1831
1845
  flex-shrink: 0;
1832
1846
  color: color-mix(in srgb, var(--text_primary, #0a0a0a) 45%, transparent);
1833
1847
  cursor: pointer;
1834
1848
  transition: color 160ms ease, background-color 160ms ease;
1835
-
1836
- &:hover {
1837
- background-color: color-mix(in srgb, var(--text_primary, #0a0a0a) 8%, transparent);
1838
- color: color-mix(in srgb, var(--text_primary, #0a0a0a) 72%, transparent);
1839
- }
1840
1849
  `;
1841
1850
  const RecordTrack = styled.div `
1842
1851
  flex: 1;
@@ -1852,7 +1861,8 @@ const RecordTrack = styled.div `
1852
1861
  const ButtonSlot = styled.div `
1853
1862
  display: flex;
1854
1863
  align-items: center;
1855
- gap: 2px;
1864
+ gap: 4px;
1865
+ padding: 2px;
1856
1866
  flex-shrink: 0;
1857
1867
  `;
1858
1868
  const StopButton = styled.button `
@@ -1989,19 +1999,39 @@ const VoiceRecorderInput = (props) => {
1989
1999
  const waveformRef = React.useRef(null);
1990
2000
  const { recorder, isSending, isPreview, timelineLevels, lastDurationRef, handleResume, handlePause, handleCancel, handleSendRequest, } = useVoiceRecorderInput(inputOptions);
1991
2001
  const { isPlaying, isRecording, recordingTime, hasRecording, previewLevels, playbackTime, formatTime, togglePlayback, } = recorder;
1992
- const getVisibleBarsCount = React.useCallback(() => {
2002
+ const [visibleBarsCount, setVisibleBarsCount] = React.useState(0);
2003
+ const measureBars = () => {
1993
2004
  const el = waveformRef.current;
1994
2005
  if (!el) {
1995
- return 60;
2006
+ return;
1996
2007
  }
1997
- return Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP));
2008
+ setVisibleBarsCount(Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP)));
2009
+ };
2010
+ /** Sync recalc before paint — catches layout shifts from button changes */
2011
+ React.useLayoutEffect(measureBars);
2012
+ /** ResizeObserver for external size changes (window resize, container resize) */
2013
+ React.useEffect(() => {
2014
+ const el = waveformRef.current;
2015
+ if (!el) {
2016
+ return;
2017
+ }
2018
+ let rafId = 0;
2019
+ const observer = new ResizeObserver(() => {
2020
+ cancelAnimationFrame(rafId);
2021
+ rafId = requestAnimationFrame(measureBars);
2022
+ });
2023
+ observer.observe(el);
2024
+ return () => {
2025
+ cancelAnimationFrame(rafId);
2026
+ observer.disconnect();
2027
+ };
1998
2028
  }, []);
1999
2029
  const displayTime = (isPreview && isPlaying)
2000
2030
  ? formatTime(playbackTime)
2001
2031
  : formatTime(recordingTime || lastDurationRef.current);
2002
- return (jsxRuntime.jsxs(Wrapper, { className: className, children: [jsxRuntime.jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsxRuntime.jsx(icons.Icon28CancelOutline, { width: 20, height: 20 }) }), jsxRuntime.jsxs(RecordTrack, { children: [jsxRuntime.jsx(ButtonSlot, { children: isPreview ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(MicIndicator, { type: "button", onClick: handleResume, "aria-label": "Resume recording", children: jsxRuntime.jsx(icons.Icon20VoiceOutline, { width: 14, height: 14 }) }), jsxRuntime.jsx(PlayPauseButton, { type: "button", onClick: togglePlayback, "aria-label": isPlaying ? 'Pause' : 'Play', children: isPlaying
2032
+ return (jsxRuntime.jsxs(Wrapper, { className: className, children: [jsxRuntime.jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsxRuntime.jsx(icons.Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 28, height: 28, style: { minWidth: 28 } }) }), jsxRuntime.jsxs(RecordTrack, { children: [jsxRuntime.jsx(ButtonSlot, { children: isPreview ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(MicIndicator, { type: "button", onClick: handleResume, "aria-label": "Resume recording", children: jsxRuntime.jsx(icons.Icon20Voice, { width: 14, height: 14 }) }), jsxRuntime.jsx(PlayPauseButton, { type: "button", onClick: togglePlayback, "aria-label": isPlaying ? 'Pause' : 'Play', children: isPlaying
2003
2033
  ? jsxRuntime.jsx(icons.Icon16Pause, { width: 14, height: 14 })
2004
- : jsxRuntime.jsx(icons.Icon16Play, { width: 14, height: 14 }) })] })) : (jsxRuntime.jsx(StopButton, { type: "button", onClick: handlePause, "aria-label": "Pause recording", children: jsxRuntime.jsx(icons.Icon16Square, { width: 12, height: 12 }) })) }), jsxRuntime.jsx(WaveformArea, { ref: waveformRef, children: isPreview ? (jsxRuntime.jsx(PreviewBars, { timelineLevels: timelineLevels, previewLevels: previewLevels, isPlaying: isPlaying, playbackTime: playbackTime, recordingTime: recordingTime, lastDuration: lastDurationRef.current, getVisibleBarsCount: getVisibleBarsCount })) : (jsxRuntime.jsx(TimelineBars, { timelineLevels: timelineLevels, getVisibleBarsCount: getVisibleBarsCount })) }), jsxRuntime.jsx(Timer, { children: displayTime })] }), jsxRuntime.jsx(SendButton, { type: "button", onClick: handleSendRequest, disabled: isSending || (!hasRecording && !isRecording), "aria-label": "Send", children: isSending ? (jsxRuntime.jsx(SpinnerIcon, {})) : (jsxRuntime.jsx(icons.Icon28SendOutline, {})) })] }));
2034
+ : jsxRuntime.jsx(icons.Icon16Play, { width: 14, height: 14 }) })] })) : (jsxRuntime.jsx(StopButton, { type: "button", onClick: handlePause, "aria-label": "Pause recording", children: jsxRuntime.jsx(icons.Icon16Pause, { width: 14, height: 14 }) })) }), jsxRuntime.jsx(WaveformArea, { ref: waveformRef, children: isPreview ? (jsxRuntime.jsx(PreviewBars, { timelineLevels: timelineLevels, previewLevels: previewLevels, isPlaying: isPlaying, playbackTime: playbackTime, recordingTime: recordingTime, lastDuration: lastDurationRef.current, visibleBarsCount: visibleBarsCount })) : (jsxRuntime.jsx(TimelineBars, { timelineLevels: timelineLevels, visibleBarsCount: visibleBarsCount })) }), jsxRuntime.jsx(Timer, { children: displayTime })] }), jsxRuntime.jsx(SendButton, { type: "button", onClick: handleSendRequest, disabled: isSending || (!hasRecording && !isRecording), "aria-label": "Send", children: isSending ? (jsxRuntime.jsx(SpinnerIcon, {})) : (jsxRuntime.jsx(icons.Icon28SendOutline, {})) })] }));
2005
2035
  };
2006
2036
 
2007
2037
  /**
@@ -2130,8 +2160,9 @@ const BAR_GAP = 2;
2130
2160
  const BAR_MIN_HEIGHT = 2;
2131
2161
  const BAR_MAX_HEIGHT = 24;
2132
2162
  const VoiceMessagePlayer = React.forwardRef((props, ref) => {
2133
- const { src, onClick, onEnded, chainId, duration, className, waveformData, } = props;
2163
+ const { src, onClick, onEnded, chainId, duration, className, waveformData, autoPreload = true, } = props;
2134
2164
  const trackRef = React.useRef(null);
2165
+ const wrapperRef = React.useRef(null);
2135
2166
  const [trackWidth, setTrackWidth] = React.useState(0);
2136
2167
  const handleEnded = React.useCallback(() => {
2137
2168
  onEnded === null || onEnded === void 0 ? void 0 : onEnded();
@@ -2139,25 +2170,66 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
2139
2170
  VoicePlaybackChain.playNext(chainId);
2140
2171
  }
2141
2172
  }, [onEnded, chainId]);
2142
- const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
2173
+ const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
2143
2174
  src,
2144
2175
  duration,
2145
2176
  waveformData,
2146
2177
  preloadedBlob: props.preloadedBlob,
2147
2178
  onEnded: handleEnded,
2148
2179
  });
2149
- React.useImperativeHandle(ref, () => ({ play }), [play]);
2180
+ React.useImperativeHandle(ref, () => ({ play, prefetch }), [play, prefetch]);
2181
+ /** Auto-preload audio when player enters viewport */
2182
+ React.useEffect(() => {
2183
+ if (!autoPreload || isLoaded || isLoading) {
2184
+ return;
2185
+ }
2186
+ const el = wrapperRef.current;
2187
+ if (!el) {
2188
+ return;
2189
+ }
2190
+ const observer = new IntersectionObserver(([entry]) => {
2191
+ if (entry.isIntersecting) {
2192
+ prefetch();
2193
+ observer.disconnect();
2194
+ }
2195
+ }, { threshold: 0.1 });
2196
+ observer.observe(el);
2197
+ return () => observer.disconnect();
2198
+ }, [
2199
+ isLoaded,
2200
+ prefetch,
2201
+ isLoading,
2202
+ autoPreload,
2203
+ ]);
2150
2204
  /** Register in playback chain for sequential playback */
2151
2205
  React.useEffect(() => {
2152
2206
  if (chainId != null) {
2153
2207
  return VoicePlaybackChain.register(chainId, () => play());
2154
2208
  }
2155
2209
  }, [chainId, play]);
2156
- /** Measure track width once after mount */
2157
- React.useLayoutEffect(() => {
2210
+ const measureTrackWidth = () => {
2158
2211
  if (trackRef.current) {
2159
2212
  setTrackWidth(trackRef.current.clientWidth);
2160
2213
  }
2214
+ };
2215
+ /** Sync recalc before paint — catches layout shifts */
2216
+ React.useLayoutEffect(measureTrackWidth);
2217
+ /** ResizeObserver for external size changes (window resize, container resize) */
2218
+ React.useEffect(() => {
2219
+ const el = trackRef.current;
2220
+ if (!el) {
2221
+ return;
2222
+ }
2223
+ let rafId = 0;
2224
+ const observer = new ResizeObserver(() => {
2225
+ cancelAnimationFrame(rafId);
2226
+ rafId = requestAnimationFrame(measureTrackWidth);
2227
+ });
2228
+ observer.observe(el);
2229
+ return () => {
2230
+ cancelAnimationFrame(rafId);
2231
+ observer.disconnect();
2232
+ };
2161
2233
  }, []);
2162
2234
  const displayTime = isPlaying
2163
2235
  ? formatTime(currentTime)
@@ -2193,7 +2265,7 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
2193
2265
  const peak = Math.max(...raw, 1);
2194
2266
  return raw.map((v) => (v / peak) * 100);
2195
2267
  }, [waveformLevels, trackWidth]);
2196
- return (jsxRuntime.jsxs(PlayerWrapper, { onClick: onClick, className: className, children: [jsxRuntime.jsx(PlayButton, { type: "button", disabled: isLoading || (!src && !props.preloadedBlob), onClick: isLoaded ? togglePlayback : preload, "aria-label": isPlaying ? 'Pause' : 'Play', children: isLoading ? (jsxRuntime.jsx(LoadingSpinner, {})) : isPlaying ? (jsxRuntime.jsx(icons.Icon24Pause, {})) : (jsxRuntime.jsx(icons.Icon24Play, {})) }), jsxRuntime.jsxs(ContentColumn, { children: [jsxRuntime.jsx(WaveformTrack, { ref: trackRef, onClick: handleTrackClick, children: interpolatedBars.map((level, i) => {
2268
+ return (jsxRuntime.jsxs(PlayerWrapper, { ref: wrapperRef, onClick: onClick, className: className, children: [jsxRuntime.jsx(PlayButton, { type: "button", disabled: isLoading || (!src && !props.preloadedBlob), onClick: isLoaded ? togglePlayback : preload, "aria-label": isPlaying ? 'Pause' : 'Play', children: isLoading ? (jsxRuntime.jsx(LoadingSpinner, {})) : isPlaying ? (jsxRuntime.jsx(icons.Icon24Pause, {})) : (jsxRuntime.jsx(icons.Icon24Play, {})) }), jsxRuntime.jsxs(ContentColumn, { children: [jsxRuntime.jsx(WaveformTrack, { ref: trackRef, onClick: handleTrackClick, children: interpolatedBars.map((level, i) => {
2197
2269
  const normalized = Math.min(100, level);
2198
2270
  const height = BAR_MIN_HEIGHT + (normalized / 100) * (BAR_MAX_HEIGHT - BAR_MIN_HEIGHT);
2199
2271
  const barProgress = interpolatedBars.length > 0