analytica-frontend-lib 1.1.10 → 1.1.11

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.
@@ -1,5 +1,10 @@
1
1
  // src/components/VideoPlayer/VideoPlayer.tsx
2
- import { useRef, useState, useEffect, useCallback } from "react";
2
+ import {
3
+ useRef,
4
+ useState,
5
+ useEffect,
6
+ useCallback
7
+ } from "react";
3
8
  import {
4
9
  Play,
5
10
  Pause,
@@ -130,6 +135,85 @@ var formatTime = (seconds) => {
130
135
  const secs = Math.floor(seconds % 60);
131
136
  return `${mins}:${secs.toString().padStart(2, "0")}`;
132
137
  };
138
+ var ProgressBar = ({
139
+ currentTime,
140
+ duration,
141
+ progressPercentage,
142
+ onSeek
143
+ }) => /* @__PURE__ */ jsx3("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsx3(
144
+ "input",
145
+ {
146
+ type: "range",
147
+ min: 0,
148
+ max: duration || 100,
149
+ value: currentTime,
150
+ onChange: (e) => onSeek(parseFloat(e.target.value)),
151
+ className: "w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500",
152
+ "aria-label": "Video progress",
153
+ style: {
154
+ background: `linear-gradient(to right, var(--color-primary-700) ${progressPercentage}%, var(--color-secondary-300) ${progressPercentage}%)`
155
+ }
156
+ }
157
+ ) });
158
+ var VolumeControls = ({
159
+ volume,
160
+ isMuted,
161
+ onVolumeChange,
162
+ onToggleMute
163
+ }) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
164
+ /* @__PURE__ */ jsx3(
165
+ IconButton_default,
166
+ {
167
+ icon: isMuted ? /* @__PURE__ */ jsx3(SpeakerSlash, { size: 24 }) : /* @__PURE__ */ jsx3(SpeakerHigh, { size: 24 }),
168
+ onClick: onToggleMute,
169
+ "aria-label": isMuted ? "Unmute" : "Mute",
170
+ className: "!bg-transparent !text-white hover:!bg-white/20"
171
+ }
172
+ ),
173
+ /* @__PURE__ */ jsx3(
174
+ "input",
175
+ {
176
+ type: "range",
177
+ min: 0,
178
+ max: 100,
179
+ value: Math.round(volume * 100),
180
+ onChange: (e) => onVolumeChange(parseInt(e.target.value)),
181
+ className: "w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500",
182
+ "aria-label": "Volume control",
183
+ style: {
184
+ background: `linear-gradient(to right, var(--color-primary-700) ${volume * 100}%, var(--color-secondary-300) ${volume * 100}%)`
185
+ }
186
+ }
187
+ )
188
+ ] });
189
+ var SpeedMenu = ({
190
+ showSpeedMenu,
191
+ playbackRate,
192
+ onToggleMenu,
193
+ onSpeedChange
194
+ }) => /* @__PURE__ */ jsxs("div", { className: "relative", children: [
195
+ /* @__PURE__ */ jsx3(
196
+ IconButton_default,
197
+ {
198
+ icon: /* @__PURE__ */ jsx3(DotsThreeVertical, { size: 24 }),
199
+ onClick: onToggleMenu,
200
+ "aria-label": "Playback speed",
201
+ className: "!bg-transparent !text-white hover:!bg-white/20"
202
+ }
203
+ ),
204
+ showSpeedMenu && /* @__PURE__ */ jsx3("div", { className: "absolute bottom-12 right-0 bg-black/90 rounded-lg p-2 min-w-20", children: [0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => /* @__PURE__ */ jsxs(
205
+ "button",
206
+ {
207
+ onClick: () => onSpeedChange(speed),
208
+ className: `block w-full text-left px-3 py-1 text-sm rounded hover:bg-white/20 transition-colors ${playbackRate === speed ? "text-primary-400" : "text-white"}`,
209
+ children: [
210
+ speed,
211
+ "x"
212
+ ]
213
+ },
214
+ speed
215
+ )) })
216
+ ] });
133
217
  var VideoPlayer = ({
134
218
  src,
135
219
  poster,
@@ -154,10 +238,51 @@ var VideoPlayer = ({
154
238
  const [showControls, setShowControls] = useState(true);
155
239
  const [hasCompleted, setHasCompleted] = useState(false);
156
240
  const [showCaptions, setShowCaptions] = useState(false);
241
+ useEffect(() => {
242
+ setHasCompleted(false);
243
+ }, [src]);
157
244
  const [playbackRate, setPlaybackRate] = useState(1);
158
245
  const [showSpeedMenu, setShowSpeedMenu] = useState(false);
159
246
  const lastSaveTimeRef = useRef(0);
160
247
  const trackRef = useRef(null);
248
+ const controlsTimeoutRef = useRef(null);
249
+ const lastMousePositionRef = useRef({ x: 0, y: 0 });
250
+ const mouseMoveTimeoutRef = useRef(null);
251
+ const clearControlsTimeout = useCallback(() => {
252
+ if (controlsTimeoutRef.current) {
253
+ clearTimeout(controlsTimeoutRef.current);
254
+ controlsTimeoutRef.current = null;
255
+ }
256
+ }, []);
257
+ const clearMouseMoveTimeout = useCallback(() => {
258
+ if (mouseMoveTimeoutRef.current) {
259
+ clearTimeout(mouseMoveTimeoutRef.current);
260
+ mouseMoveTimeoutRef.current = null;
261
+ }
262
+ }, []);
263
+ const showControlsWithTimer = useCallback(() => {
264
+ setShowControls(true);
265
+ clearControlsTimeout();
266
+ if (isPlaying) {
267
+ const timeout = isFullscreen ? 2e3 : 3e3;
268
+ controlsTimeoutRef.current = window.setTimeout(() => {
269
+ setShowControls(false);
270
+ }, timeout);
271
+ }
272
+ }, [isPlaying, isFullscreen, clearControlsTimeout]);
273
+ const handleMouseMove = useCallback(
274
+ (event) => {
275
+ const currentX = event.clientX;
276
+ const currentY = event.clientY;
277
+ const lastPos = lastMousePositionRef.current;
278
+ const hasMoved = Math.abs(currentX - lastPos.x) > 5 || Math.abs(currentY - lastPos.y) > 5;
279
+ if (hasMoved) {
280
+ lastMousePositionRef.current = { x: currentX, y: currentY };
281
+ showControlsWithTimer();
282
+ }
283
+ },
284
+ [showControlsWithTimer]
285
+ );
161
286
  useEffect(() => {
162
287
  if (videoRef.current) {
163
288
  videoRef.current.volume = volume;
@@ -165,84 +290,129 @@ var VideoPlayer = ({
165
290
  }
166
291
  }, [volume, isMuted]);
167
292
  useEffect(() => {
168
- if (!autoSave || !storageKey) return;
169
- const raw = localStorage.getItem(`${storageKey}-${src}`);
170
- const saved = raw !== null ? Number(raw) : NaN;
171
- const hasValidSaved = Number.isFinite(saved) && saved >= 0;
172
- const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0;
173
- let start;
174
- if (hasValidInitial) {
175
- start = initialTime;
176
- } else if (hasValidSaved) {
177
- start = saved;
293
+ const video = videoRef.current;
294
+ if (!video) return;
295
+ const onPlay = () => setIsPlaying(true);
296
+ const onPause = () => setIsPlaying(false);
297
+ const onEnded = () => setIsPlaying(false);
298
+ video.addEventListener("play", onPlay);
299
+ video.addEventListener("pause", onPause);
300
+ video.addEventListener("ended", onEnded);
301
+ return () => {
302
+ video.removeEventListener("play", onPlay);
303
+ video.removeEventListener("pause", onPause);
304
+ video.removeEventListener("ended", onEnded);
305
+ };
306
+ }, []);
307
+ useEffect(() => {
308
+ if (isPlaying) {
309
+ showControlsWithTimer();
178
310
  } else {
179
- start = void 0;
311
+ clearControlsTimeout();
312
+ setShowControls(true);
313
+ }
314
+ }, [isPlaying, showControlsWithTimer, clearControlsTimeout]);
315
+ useEffect(() => {
316
+ const handleFullscreenChange = () => {
317
+ const isCurrentlyFullscreen = !!document.fullscreenElement;
318
+ setIsFullscreen(isCurrentlyFullscreen);
319
+ if (isCurrentlyFullscreen) {
320
+ showControlsWithTimer();
321
+ }
322
+ };
323
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
324
+ return () => {
325
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
326
+ };
327
+ }, [showControlsWithTimer]);
328
+ const getInitialTime = useCallback(() => {
329
+ if (!autoSave || !storageKey) {
330
+ return Number.isFinite(initialTime) && initialTime >= 0 ? initialTime : void 0;
180
331
  }
332
+ const saved = Number(localStorage.getItem(`${storageKey}-${src}`) || NaN);
333
+ const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0;
334
+ const hasValidSaved = Number.isFinite(saved) && saved >= 0;
335
+ if (hasValidInitial) return initialTime;
336
+ if (hasValidSaved) return saved;
337
+ return void 0;
338
+ }, [autoSave, storageKey, src, initialTime]);
339
+ useEffect(() => {
340
+ const start = getInitialTime();
181
341
  if (start !== void 0 && videoRef.current) {
182
342
  videoRef.current.currentTime = start;
183
343
  setCurrentTime(start);
184
344
  }
185
- }, [src, storageKey, autoSave, initialTime]);
186
- const saveProgress = useCallback(() => {
187
- if (!autoSave || !storageKey) return;
188
- const now = Date.now();
189
- if (now - lastSaveTimeRef.current > 5e3) {
190
- localStorage.setItem(`${storageKey}-${src}`, currentTime.toString());
191
- lastSaveTimeRef.current = now;
192
- }
193
- }, [autoSave, storageKey, src, currentTime]);
194
- const togglePlayPause = useCallback(() => {
195
- if (videoRef.current) {
196
- if (isPlaying) {
197
- videoRef.current.pause();
198
- } else {
199
- videoRef.current.play();
345
+ }, [getInitialTime]);
346
+ const saveProgress = useCallback(
347
+ (time) => {
348
+ if (!autoSave || !storageKey) return;
349
+ const now = Date.now();
350
+ if (now - lastSaveTimeRef.current > 5e3) {
351
+ localStorage.setItem(`${storageKey}-${src}`, time.toString());
352
+ lastSaveTimeRef.current = now;
200
353
  }
201
- setIsPlaying(!isPlaying);
354
+ },
355
+ [autoSave, storageKey, src]
356
+ );
357
+ const togglePlayPause = useCallback(async () => {
358
+ const video = videoRef.current;
359
+ if (!video) return;
360
+ if (!video.paused) {
361
+ video.pause();
362
+ return;
363
+ }
364
+ try {
365
+ await video.play();
366
+ } catch {
202
367
  }
203
- }, [isPlaying]);
368
+ }, []);
204
369
  const handleVolumeChange = useCallback(
205
370
  (newVolume) => {
206
- if (videoRef.current) {
207
- const volumeValue = newVolume / 100;
208
- videoRef.current.volume = volumeValue;
209
- setVolume(volumeValue);
210
- if (volumeValue === 0) {
211
- videoRef.current.muted = true;
212
- setIsMuted(true);
213
- } else if (isMuted) {
214
- videoRef.current.muted = false;
215
- setIsMuted(false);
216
- }
371
+ const video = videoRef.current;
372
+ if (!video) return;
373
+ const volumeValue = newVolume / 100;
374
+ video.volume = volumeValue;
375
+ setVolume(volumeValue);
376
+ const shouldMute = volumeValue === 0;
377
+ const shouldUnmute = volumeValue > 0 && isMuted;
378
+ if (shouldMute) {
379
+ video.muted = true;
380
+ setIsMuted(true);
381
+ } else if (shouldUnmute) {
382
+ video.muted = false;
383
+ setIsMuted(false);
217
384
  }
218
385
  },
219
386
  [isMuted]
220
387
  );
221
388
  const toggleMute = useCallback(() => {
222
- if (videoRef.current) {
223
- if (isMuted) {
224
- const restoreVolume = volume > 0 ? volume : 0.5;
225
- videoRef.current.volume = restoreVolume;
226
- videoRef.current.muted = false;
227
- setVolume(restoreVolume);
228
- setIsMuted(false);
229
- } else {
230
- videoRef.current.muted = true;
231
- setIsMuted(true);
232
- }
389
+ const video = videoRef.current;
390
+ if (!video) return;
391
+ if (isMuted) {
392
+ const restoreVolume = volume > 0 ? volume : 0.5;
393
+ video.volume = restoreVolume;
394
+ video.muted = false;
395
+ setVolume(restoreVolume);
396
+ setIsMuted(false);
397
+ } else {
398
+ video.muted = true;
399
+ setIsMuted(true);
233
400
  }
234
401
  }, [isMuted, volume]);
402
+ const handleSeek = useCallback((newTime) => {
403
+ const video = videoRef.current;
404
+ if (video) {
405
+ video.currentTime = newTime;
406
+ }
407
+ }, []);
235
408
  const toggleFullscreen = useCallback(() => {
236
409
  const container = videoRef.current?.parentElement;
237
410
  if (!container) return;
238
- if (!isFullscreen) {
239
- if (container.requestFullscreen) {
240
- container.requestFullscreen();
241
- }
242
- } else if (document.exitFullscreen) {
411
+ if (!isFullscreen && container.requestFullscreen) {
412
+ container.requestFullscreen();
413
+ } else if (isFullscreen && document.exitFullscreen) {
243
414
  document.exitFullscreen();
244
415
  }
245
- setIsFullscreen(!isFullscreen);
246
416
  }, [isFullscreen]);
247
417
  const handleSpeedChange = useCallback((speed) => {
248
418
  if (videoRef.current) {
@@ -260,29 +430,28 @@ var VideoPlayer = ({
260
430
  setShowCaptions(newShowCaptions);
261
431
  trackRef.current.track.mode = newShowCaptions && subtitles ? "showing" : "hidden";
262
432
  }, [showCaptions, subtitles]);
263
- const handleTimeUpdate = useCallback(() => {
264
- if (videoRef.current) {
265
- const current = videoRef.current.currentTime;
266
- setCurrentTime(current);
267
- saveProgress();
268
- onTimeUpdate?.(current);
269
- if (duration > 0) {
270
- const progressPercent = current / duration * 100;
271
- onProgress?.(progressPercent);
272
- if (progressPercent >= 95 && !hasCompleted) {
273
- setHasCompleted(true);
274
- onVideoComplete?.();
275
- }
433
+ const checkVideoCompletion = useCallback(
434
+ (progressPercent) => {
435
+ if (progressPercent >= 95 && !hasCompleted) {
436
+ setHasCompleted(true);
437
+ onVideoComplete?.();
276
438
  }
439
+ },
440
+ [hasCompleted, onVideoComplete]
441
+ );
442
+ const handleTimeUpdate = useCallback(() => {
443
+ const video = videoRef.current;
444
+ if (!video) return;
445
+ const current = video.currentTime;
446
+ setCurrentTime(current);
447
+ saveProgress(current);
448
+ onTimeUpdate?.(current);
449
+ if (duration > 0) {
450
+ const progressPercent = current / duration * 100;
451
+ onProgress?.(progressPercent);
452
+ checkVideoCompletion(progressPercent);
277
453
  }
278
- }, [
279
- duration,
280
- saveProgress,
281
- onTimeUpdate,
282
- onProgress,
283
- onVideoComplete,
284
- hasCompleted
285
- ]);
454
+ }, [duration, saveProgress, onTimeUpdate, onProgress, checkVideoCompletion]);
286
455
  const handleLoadedMetadata = useCallback(() => {
287
456
  if (videoRef.current) {
288
457
  setDuration(videoRef.current.duration);
@@ -311,9 +480,78 @@ var VideoPlayer = ({
311
480
  return () => {
312
481
  document.removeEventListener("visibilitychange", handleVisibilityChange);
313
482
  window.removeEventListener("blur", handleBlur);
483
+ clearControlsTimeout();
484
+ clearMouseMoveTimeout();
314
485
  };
315
- }, [isPlaying]);
486
+ }, [isPlaying, clearControlsTimeout, clearMouseMoveTimeout]);
316
487
  const progressPercentage = duration > 0 ? currentTime / duration * 100 : 0;
488
+ const getTopControlsOpacity = useCallback(() => {
489
+ if (isFullscreen) {
490
+ return showControls ? "opacity-100" : "opacity-0";
491
+ }
492
+ return !isPlaying || showControls ? "opacity-100" : "opacity-0 group-hover:opacity-100";
493
+ }, [isFullscreen, showControls, isPlaying]);
494
+ const getBottomControlsOpacity = useCallback(() => {
495
+ if (isFullscreen) {
496
+ return showControls ? "opacity-100" : "opacity-0";
497
+ }
498
+ return !isPlaying || showControls ? "opacity-100" : "opacity-0 group-hover:opacity-100";
499
+ }, [isFullscreen, showControls, isPlaying]);
500
+ const handleVideoKeyDown = useCallback(
501
+ (e) => {
502
+ if (e.key) {
503
+ e.stopPropagation();
504
+ showControlsWithTimer();
505
+ }
506
+ switch (e.key) {
507
+ case " ":
508
+ case "Enter":
509
+ e.preventDefault();
510
+ togglePlayPause();
511
+ break;
512
+ case "ArrowLeft":
513
+ e.preventDefault();
514
+ if (videoRef.current) {
515
+ videoRef.current.currentTime -= 10;
516
+ }
517
+ break;
518
+ case "ArrowRight":
519
+ e.preventDefault();
520
+ if (videoRef.current) {
521
+ videoRef.current.currentTime += 10;
522
+ }
523
+ break;
524
+ case "ArrowUp":
525
+ e.preventDefault();
526
+ handleVolumeChange(Math.min(100, volume * 100 + 10));
527
+ break;
528
+ case "ArrowDown":
529
+ e.preventDefault();
530
+ handleVolumeChange(Math.max(0, volume * 100 - 10));
531
+ break;
532
+ case "m":
533
+ case "M":
534
+ e.preventDefault();
535
+ toggleMute();
536
+ break;
537
+ case "f":
538
+ case "F":
539
+ e.preventDefault();
540
+ toggleFullscreen();
541
+ break;
542
+ default:
543
+ break;
544
+ }
545
+ },
546
+ [
547
+ showControlsWithTimer,
548
+ togglePlayPause,
549
+ handleVolumeChange,
550
+ volume,
551
+ toggleMute,
552
+ toggleFullscreen
553
+ ]
554
+ );
317
555
  return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col", className), children: [
318
556
  (title || subtitleText) && /* @__PURE__ */ jsx3("div", { className: "bg-subject-1 rounded-t-xl px-8 py-4 flex items-end justify-between min-h-20", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
319
557
  title && /* @__PURE__ */ jsx3(
@@ -340,12 +578,17 @@ var VideoPlayer = ({
340
578
  )
341
579
  ] }) }),
342
580
  /* @__PURE__ */ jsxs(
343
- "div",
581
+ "section",
344
582
  {
345
583
  className: cn(
346
584
  "relative w-full bg-background overflow-hidden group",
347
- title || subtitleText ? "rounded-b-xl" : "rounded-xl"
585
+ title || subtitleText ? "rounded-b-xl" : "rounded-xl",
586
+ // Hide cursor when controls are hidden and video is playing in fullscreen
587
+ isFullscreen && isPlaying && !showControls ? "cursor-none" : "cursor-default"
348
588
  ),
589
+ "aria-label": title ? `Video player: ${title}` : "Video player",
590
+ onMouseMove: isFullscreen ? handleMouseMove : showControlsWithTimer,
591
+ onMouseEnter: showControlsWithTimer,
349
592
  children: [
350
593
  /* @__PURE__ */ jsx3(
351
594
  "video",
@@ -358,39 +601,7 @@ var VideoPlayer = ({
358
601
  onTimeUpdate: handleTimeUpdate,
359
602
  onLoadedMetadata: handleLoadedMetadata,
360
603
  onClick: togglePlayPause,
361
- onKeyDown: (e) => {
362
- if (e.key) {
363
- setShowControls(true);
364
- }
365
- if (e.key === " " || e.key === "Enter") {
366
- e.preventDefault();
367
- togglePlayPause();
368
- }
369
- if (e.key === "ArrowLeft" && videoRef.current) {
370
- e.preventDefault();
371
- videoRef.current.currentTime -= 10;
372
- }
373
- if (e.key === "ArrowRight" && videoRef.current) {
374
- e.preventDefault();
375
- videoRef.current.currentTime += 10;
376
- }
377
- if (e.key === "ArrowUp") {
378
- e.preventDefault();
379
- handleVolumeChange(Math.min(100, volume * 100 + 10));
380
- }
381
- if (e.key === "ArrowDown") {
382
- e.preventDefault();
383
- handleVolumeChange(Math.max(0, volume * 100 - 10));
384
- }
385
- if (e.key === "m" || e.key === "M") {
386
- e.preventDefault();
387
- toggleMute();
388
- }
389
- if (e.key === "f" || e.key === "F") {
390
- e.preventDefault();
391
- toggleFullscreen();
392
- }
393
- },
604
+ onKeyDown: handleVideoKeyDown,
394
605
  tabIndex: 0,
395
606
  "aria-label": title ? `Video: ${title}` : "Video player",
396
607
  children: /* @__PURE__ */ jsx3(
@@ -420,9 +631,9 @@ var VideoPlayer = ({
420
631
  {
421
632
  className: cn(
422
633
  "absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/70 to-transparent transition-opacity",
423
- !isPlaying || showControls ? "opacity-100" : "opacity-0 group-hover:opacity-100"
634
+ getTopControlsOpacity()
424
635
  ),
425
- children: /* @__PURE__ */ jsx3("div", { className: "ml-auto block", children: /* @__PURE__ */ jsx3(
636
+ children: /* @__PURE__ */ jsx3("div", { className: "flex justify-start", children: /* @__PURE__ */ jsx3(
426
637
  IconButton_default,
427
638
  {
428
639
  icon: isFullscreen ? /* @__PURE__ */ jsx3(ArrowsInSimple, { size: 24 }) : /* @__PURE__ */ jsx3(ArrowsOutSimple, { size: 24 }),
@@ -438,29 +649,18 @@ var VideoPlayer = ({
438
649
  {
439
650
  className: cn(
440
651
  "absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent transition-opacity",
441
- !isPlaying || showControls ? "opacity-100" : "opacity-0 group-hover:opacity-100"
652
+ getBottomControlsOpacity()
442
653
  ),
443
654
  children: [
444
- /* @__PURE__ */ jsx3("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsx3(
445
- "input",
655
+ /* @__PURE__ */ jsx3(
656
+ ProgressBar,
446
657
  {
447
- type: "range",
448
- min: 0,
449
- max: duration || 100,
450
- value: currentTime,
451
- onChange: (e) => {
452
- const newTime = parseFloat(e.target.value);
453
- if (videoRef.current) {
454
- videoRef.current.currentTime = newTime;
455
- }
456
- },
457
- className: "w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500",
458
- "aria-label": "Video progress",
459
- style: {
460
- background: `linear-gradient(to right, #2271C4 ${progressPercentage}%, #D5D4D4 ${progressPercentage}%)`
461
- }
658
+ currentTime,
659
+ duration,
660
+ progressPercentage,
661
+ onSeek: handleSeek
462
662
  }
463
- ) }),
663
+ ),
464
664
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 pb-4", children: [
465
665
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
466
666
  /* @__PURE__ */ jsx3(
@@ -472,32 +672,15 @@ var VideoPlayer = ({
472
672
  className: "!bg-transparent !text-white hover:!bg-white/20"
473
673
  }
474
674
  ),
475
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
476
- /* @__PURE__ */ jsx3(
477
- IconButton_default,
478
- {
479
- icon: isMuted ? /* @__PURE__ */ jsx3(SpeakerSlash, { size: 24 }) : /* @__PURE__ */ jsx3(SpeakerHigh, { size: 24 }),
480
- onClick: toggleMute,
481
- "aria-label": isMuted ? "Unmute" : "Mute",
482
- className: "!bg-transparent !text-white hover:!bg-white/20"
483
- }
484
- ),
485
- /* @__PURE__ */ jsx3(
486
- "input",
487
- {
488
- type: "range",
489
- min: 0,
490
- max: 100,
491
- value: Math.round(volume * 100),
492
- onChange: (e) => handleVolumeChange(parseInt(e.target.value)),
493
- className: "w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500",
494
- "aria-label": "Volume control",
495
- style: {
496
- background: `linear-gradient(to right, #2271C4 ${volume * 100}%, #D5D4D4 ${volume * 100}%)`
497
- }
498
- }
499
- )
500
- ] }),
675
+ /* @__PURE__ */ jsx3(
676
+ VolumeControls,
677
+ {
678
+ volume,
679
+ isMuted,
680
+ onVolumeChange: handleVolumeChange,
681
+ onToggleMute: toggleMute
682
+ }
683
+ ),
501
684
  subtitles && /* @__PURE__ */ jsx3(
502
685
  IconButton_default,
503
686
  {
@@ -516,29 +699,15 @@ var VideoPlayer = ({
516
699
  formatTime(duration)
517
700
  ] })
518
701
  ] }),
519
- /* @__PURE__ */ jsx3("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
520
- /* @__PURE__ */ jsx3(
521
- IconButton_default,
522
- {
523
- icon: /* @__PURE__ */ jsx3(DotsThreeVertical, { size: 24 }),
524
- onClick: toggleSpeedMenu,
525
- "aria-label": "Playback speed",
526
- className: "!bg-transparent !text-white hover:!bg-white/20"
527
- }
528
- ),
529
- showSpeedMenu && /* @__PURE__ */ jsx3("div", { className: "absolute bottom-12 right-0 bg-black/90 rounded-lg p-2 min-w-20", children: [0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => /* @__PURE__ */ jsxs(
530
- "button",
531
- {
532
- onClick: () => handleSpeedChange(speed),
533
- className: `block w-full text-left px-3 py-1 text-sm rounded hover:bg-white/20 transition-colors ${playbackRate === speed ? "text-primary-400" : "text-white"}`,
534
- children: [
535
- speed,
536
- "x"
537
- ]
538
- },
539
- speed
540
- )) })
541
- ] }) })
702
+ /* @__PURE__ */ jsx3("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsx3(
703
+ SpeedMenu,
704
+ {
705
+ showSpeedMenu,
706
+ playbackRate,
707
+ onToggleMenu: toggleSpeedMenu,
708
+ onSpeedChange: handleSpeedChange
709
+ }
710
+ ) })
542
711
  ] })
543
712
  ]
544
713
  }