@xhub-short/ui 0.1.0-beta.1 → 0.1.0-beta.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.
Files changed (76) hide show
  1. package/dist/CommentSheet.css-DyEc3Sro.d.ts +217 -0
  2. package/dist/VideoSlotPlayIndicator-DPs8Xt5C.d.ts +51 -0
  3. package/dist/chunk-3OB3OVYR.js +349 -0
  4. package/dist/{chunk-WKX2WBVO.js → chunk-3XPJHUYL.js} +1 -39
  5. package/dist/chunk-4RIMQOBR.js +58 -0
  6. package/dist/chunk-4TUBNA2X.js +180 -0
  7. package/dist/{chunk-4YDIRPIN.js → chunk-ANCP53F3.js} +3 -3
  8. package/dist/{chunk-UXMA4KJZ.js → chunk-CAWE42LH.js} +5 -3
  9. package/dist/{chunk-ANGBSV7L.js → chunk-CIIZ3IHV.js} +10 -5
  10. package/dist/chunk-DR7KR7OT.js +103 -0
  11. package/dist/chunk-DXLCQ4FH.js +102 -0
  12. package/dist/chunk-EDWS2IPH.js +1 -0
  13. package/dist/chunk-FR7UQSZP.js +570 -0
  14. package/dist/chunk-IWSBYOSS.js +91 -0
  15. package/dist/chunk-JEY6R4KJ.js +334 -0
  16. package/dist/chunk-KMJ3PQ7M.js +1262 -0
  17. package/dist/chunk-MFJS65C5.js +368 -0
  18. package/dist/{chunk-HW4LXTFT.js → chunk-OM4L7RE5.js} +18 -6
  19. package/dist/chunk-PBIH2F2Q.js +344 -0
  20. package/dist/chunk-PJ4NMVMY.js +326 -0
  21. package/dist/chunk-Q6MG7AVG.js +531 -0
  22. package/dist/chunk-QCKVF2DR.js +713 -0
  23. package/dist/chunk-QCRRF76W.js +75 -0
  24. package/dist/chunk-QUEJHA24.js +508 -0
  25. package/dist/chunk-VXW7AOGM.js +285 -0
  26. package/dist/chunk-YB7AXTX7.js +430 -0
  27. package/dist/chunk-ZGWSJ6Z5.js +601 -0
  28. package/dist/components/ActionBar/index.js +1 -1
  29. package/dist/components/AdvanceMenu/index.d.ts +78 -0
  30. package/dist/components/AdvanceMenu/index.js +1 -0
  31. package/dist/components/AuthorInfo/index.d.ts +5 -1
  32. package/dist/components/AuthorInfo/index.js +1 -1
  33. package/dist/components/BottomSheet/index.d.ts +82 -0
  34. package/dist/components/BottomSheet/index.js +1 -0
  35. package/dist/components/CleanModeOverlay/index.d.ts +60 -0
  36. package/dist/components/CleanModeOverlay/index.js +1 -0
  37. package/dist/components/CommentSheet/index.d.ts +164 -0
  38. package/dist/components/CommentSheet/index.js +1 -0
  39. package/dist/components/DetailView/index.d.ts +311 -0
  40. package/dist/components/DetailView/index.js +1 -0
  41. package/dist/components/ErrorBoundary/index.js +1 -1
  42. package/dist/components/ImageCarousel/index.d.ts +50 -0
  43. package/dist/components/ImageCarousel/index.js +1 -0
  44. package/dist/components/ImagePostSlot/index.d.ts +207 -0
  45. package/dist/components/ImagePostSlot/index.js +1 -0
  46. package/dist/components/ProgressBar/index.d.ts +30 -2
  47. package/dist/components/ProgressBar/index.js +1 -1
  48. package/dist/components/QualityPicker/index.d.ts +35 -0
  49. package/dist/components/QualityPicker/index.js +1 -0
  50. package/dist/components/ReportSheet/index.d.ts +68 -0
  51. package/dist/components/ReportSheet/index.js +1 -0
  52. package/dist/components/Skeleton/index.js +1 -1
  53. package/dist/components/SpeedPicker/index.d.ts +32 -0
  54. package/dist/components/SpeedPicker/index.js +1 -0
  55. package/dist/components/VideoFeed/index.d.ts +12 -1
  56. package/dist/components/VideoFeed/index.js +1 -1
  57. package/dist/components/VideoInfo/index.d.ts +4 -2
  58. package/dist/components/VideoInfo/index.js +1 -1
  59. package/dist/components/VideoPlayer/index.d.ts +14 -41
  60. package/dist/components/VideoPlayer/index.js +1 -1
  61. package/dist/components/VideoSlot/index.d.ts +84 -65
  62. package/dist/components/VideoSlot/index.js +2 -1
  63. package/dist/components/VirtualSlider/index.d.ts +339 -0
  64. package/dist/components/VirtualSlider/index.js +1 -0
  65. package/dist/components/icons/index.js +1 -1
  66. package/dist/index.d.ts +107 -95
  67. package/dist/index.js +84 -27
  68. package/package.json +51 -7
  69. package/dist/chunk-2PTMP65P.js +0 -738
  70. package/dist/chunk-4MN72OZH.js +0 -148
  71. package/dist/chunk-DHQJBXQW.js +0 -562
  72. package/dist/chunk-SSJDO24Q.js +0 -204
  73. package/dist/chunk-XAOEHLOX.js +0 -1326
  74. package/dist/chunk-YW23IBKF.js +0 -530
  75. package/dist/chunk-ZZDQKP4R.js +0 -418
  76. package/dist/use-gesture-react.esm-3SV4QLEJ.js +0 -1893
@@ -0,0 +1,368 @@
1
+ import { PauseIcon, PlayIcon } from './chunk-ANCP53F3.js';
2
+ import { createContext, memo, useState, useRef, useEffect, useCallback, useContext } from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ var DEFAULT_DOUBLE_TAP_DELAY = 300;
6
+ var DEFAULT_LONG_PRESS_DELAY = 500;
7
+ var TAP_DEBOUNCE_MS = 50;
8
+ var DEFAULT_MOVEMENT_THRESHOLD = 10;
9
+ function useDoubleTap({
10
+ containerRef,
11
+ onSingleTap,
12
+ onDoubleTap,
13
+ onLongPress,
14
+ doubleTapDelay = DEFAULT_DOUBLE_TAP_DELAY,
15
+ longPressDelay = DEFAULT_LONG_PRESS_DELAY,
16
+ disabled = false,
17
+ movementThreshold = DEFAULT_MOVEMENT_THRESHOLD
18
+ }) {
19
+ const lastTapTimeRef = useRef(0);
20
+ const lastDoubleTapTimeRef = useRef(0);
21
+ const tapTimeoutRef = useRef(null);
22
+ const longPressTimeoutRef = useRef(null);
23
+ const longPressTriggeredRef = useRef(false);
24
+ const lastPositionRef = useRef({ x: 50, y: 50 });
25
+ const lastExecutionTimeRef = useRef(0);
26
+ const pointerStartRef = useRef(null);
27
+ const cumulativeMovementRef = useRef(0);
28
+ useEffect(() => {
29
+ return () => {
30
+ if (tapTimeoutRef.current) {
31
+ clearTimeout(tapTimeoutRef.current);
32
+ tapTimeoutRef.current = null;
33
+ }
34
+ if (longPressTimeoutRef.current) {
35
+ clearTimeout(longPressTimeoutRef.current);
36
+ longPressTimeoutRef.current = null;
37
+ }
38
+ };
39
+ }, []);
40
+ const calculatePosition = useCallback(
41
+ (clientX, clientY) => {
42
+ const container = containerRef.current;
43
+ if (!container) return { x: 50, y: 50 };
44
+ const rect = container.getBoundingClientRect();
45
+ const x = (clientX - rect.left) / rect.width * 100;
46
+ const y = (clientY - rect.top) / rect.height * 100;
47
+ return {
48
+ x: Math.max(0, Math.min(100, x)),
49
+ y: Math.max(0, Math.min(100, y))
50
+ };
51
+ },
52
+ [containerRef]
53
+ );
54
+ const handleTapCore = useCallback(() => {
55
+ if (disabled) return;
56
+ const now = Date.now();
57
+ const timeSinceLastTap = now - lastTapTimeRef.current;
58
+ if (tapTimeoutRef.current) {
59
+ clearTimeout(tapTimeoutRef.current);
60
+ tapTimeoutRef.current = null;
61
+ }
62
+ if (timeSinceLastTap < doubleTapDelay) {
63
+ lastDoubleTapTimeRef.current = now;
64
+ lastTapTimeRef.current = 0;
65
+ onDoubleTap?.(lastPositionRef.current);
66
+ } else {
67
+ lastTapTimeRef.current = now;
68
+ tapTimeoutRef.current = setTimeout(() => {
69
+ tapTimeoutRef.current = null;
70
+ const timeSinceDoubleTap = Date.now() - lastDoubleTapTimeRef.current;
71
+ if (lastDoubleTapTimeRef.current > 0 && timeSinceDoubleTap < doubleTapDelay * 1.5) {
72
+ return;
73
+ }
74
+ if (lastTapTimeRef.current !== now) {
75
+ return;
76
+ }
77
+ onSingleTap?.();
78
+ }, doubleTapDelay);
79
+ }
80
+ }, [disabled, doubleTapDelay, onSingleTap, onDoubleTap]);
81
+ const isSwipe = useCallback(
82
+ (clientX, clientY) => {
83
+ if (cumulativeMovementRef.current > movementThreshold * 2) {
84
+ return true;
85
+ }
86
+ const start = pointerStartRef.current;
87
+ if (!start) return false;
88
+ const dx = Math.abs(clientX - start.x);
89
+ const dy = Math.abs(clientY - start.y);
90
+ return dx > movementThreshold || dy > movementThreshold;
91
+ },
92
+ [movementThreshold]
93
+ );
94
+ const executeTapWithDebounce = useCallback(
95
+ (clientX, clientY) => {
96
+ if (disabled) return;
97
+ if (isSwipe(clientX, clientY)) {
98
+ pointerStartRef.current = null;
99
+ return;
100
+ }
101
+ const now = Date.now();
102
+ if (now - lastExecutionTimeRef.current < TAP_DEBOUNCE_MS) {
103
+ return;
104
+ }
105
+ lastExecutionTimeRef.current = now;
106
+ lastPositionRef.current = calculatePosition(clientX, clientY);
107
+ handleTapCore();
108
+ pointerStartRef.current = null;
109
+ },
110
+ [disabled, calculatePosition, handleTapCore, isSwipe]
111
+ );
112
+ const cancelLongPress = useCallback(() => {
113
+ if (longPressTimeoutRef.current) {
114
+ clearTimeout(longPressTimeoutRef.current);
115
+ longPressTimeoutRef.current = null;
116
+ }
117
+ }, []);
118
+ const handlePointerDown = useCallback(
119
+ (e) => {
120
+ if (e.button !== 0) return;
121
+ pointerStartRef.current = { x: e.clientX, y: e.clientY };
122
+ cumulativeMovementRef.current = 0;
123
+ longPressTriggeredRef.current = false;
124
+ if (onLongPress && !disabled) {
125
+ cancelLongPress();
126
+ const position = calculatePosition(e.clientX, e.clientY);
127
+ longPressTimeoutRef.current = setTimeout(() => {
128
+ longPressTimeoutRef.current = null;
129
+ if (cumulativeMovementRef.current <= movementThreshold * 2) {
130
+ longPressTriggeredRef.current = true;
131
+ onLongPress(position);
132
+ }
133
+ }, longPressDelay);
134
+ }
135
+ },
136
+ [onLongPress, disabled, longPressDelay, movementThreshold, calculatePosition, cancelLongPress]
137
+ );
138
+ const handlePointerMove = useCallback(
139
+ (e) => {
140
+ if (!pointerStartRef.current) return;
141
+ cumulativeMovementRef.current += Math.abs(e.movementX) + Math.abs(e.movementY);
142
+ if (cumulativeMovementRef.current > movementThreshold * 2) {
143
+ cancelLongPress();
144
+ }
145
+ },
146
+ [movementThreshold, cancelLongPress]
147
+ );
148
+ const handlePointerUp = useCallback(
149
+ (e) => {
150
+ if (e.button !== 0) return;
151
+ cancelLongPress();
152
+ if (longPressTriggeredRef.current) {
153
+ longPressTriggeredRef.current = false;
154
+ pointerStartRef.current = null;
155
+ return;
156
+ }
157
+ executeTapWithDebounce(e.clientX, e.clientY);
158
+ },
159
+ [executeTapWithDebounce, cancelLongPress]
160
+ );
161
+ const handleClick = useCallback(
162
+ (e) => {
163
+ if (e.detail > 0) {
164
+ executeTapWithDebounce(e.clientX, e.clientY);
165
+ }
166
+ },
167
+ [executeTapWithDebounce]
168
+ );
169
+ return {
170
+ handlers: {
171
+ onPointerDown: handlePointerDown,
172
+ onPointerMove: handlePointerMove,
173
+ onPointerUp: handlePointerUp,
174
+ onClick: handleClick
175
+ },
176
+ lastPosition: lastPositionRef
177
+ };
178
+ }
179
+ function invariantContextError(hookName, componentName) {
180
+ const baseMessage = `${hookName} must be used within a VideoSlot component.`;
181
+ if (process.env.NODE_ENV !== "production") {
182
+ const debugInfo = [
183
+ "",
184
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
185
+ "\u274C VideoSlot Context Error",
186
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
187
+ "",
188
+ `Hook: ${hookName}`,
189
+ "",
190
+ "",
191
+ "Possible causes:",
192
+ " 1. You used a VideoSlot compound component outside of <VideoSlot>",
193
+ " 2. You used the headless component without SDK wiring",
194
+ " 3. The component tree was rendered in isolation (e.g., Storybook)",
195
+ "",
196
+ "Solutions:",
197
+ " 1. Wrap with <VideoSlot> or <VideoSlotHeadless>",
198
+ " 2. Use useOptionalVideoSlotContext() if context is optional",
199
+ " 3. Provide mock context in tests via <VideoSlotContext.Provider>",
200
+ "",
201
+ "Example:",
202
+ " // Correct usage",
203
+ " <VideoSlot video={video}>",
204
+ " <VideoSlotPoster /> \u2190 These need context",
205
+ " <VideoSlotOverlay />",
206
+ " </VideoSlot>",
207
+ "",
208
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"
209
+ ].filter(Boolean).join("\n");
210
+ throw new Error(baseMessage + debugInfo);
211
+ }
212
+ throw new Error(baseMessage);
213
+ }
214
+ var VideoSlotContext = createContext(null);
215
+ VideoSlotContext.displayName = "VideoSlotContext";
216
+ function useVideoSlotContext() {
217
+ const context = useContext(VideoSlotContext);
218
+ if (!context) {
219
+ invariantContextError("useVideoSlotContext");
220
+ }
221
+ return context;
222
+ }
223
+ function useOptionalVideoSlotContext() {
224
+ return useContext(VideoSlotContext);
225
+ }
226
+ function useVideoSlotVideo() {
227
+ const { video } = useVideoSlotContext();
228
+ return video;
229
+ }
230
+ function useVideoSlotPlayer() {
231
+ const { playerState, playerControls } = useVideoSlotContext();
232
+ return { state: playerState, controls: playerControls };
233
+ }
234
+ function useVideoSlotResource() {
235
+ const { resourceState } = useVideoSlotContext();
236
+ return resourceState;
237
+ }
238
+ function useVideoSlotIsActive() {
239
+ const { resourceState } = useVideoSlotContext();
240
+ return resourceState.isActive;
241
+ }
242
+ function useVideoSlotIsPlaying() {
243
+ const { playerState } = useVideoSlotContext();
244
+ return playerState.isPlaying;
245
+ }
246
+ function DefaultPlayIcon({ size }) {
247
+ return /* @__PURE__ */ jsx(PlayIcon, { size });
248
+ }
249
+ function DefaultPauseIcon({ size }) {
250
+ return /* @__PURE__ */ jsx(PauseIcon, { size });
251
+ }
252
+ var VideoSlotPlayIndicatorInner = memo(function VideoSlotPlayIndicatorInner2({
253
+ playIcon,
254
+ pauseIcon,
255
+ size = 48,
256
+ duration = 600,
257
+ persistWhenPaused = true,
258
+ className,
259
+ testId,
260
+ lastUserToggleTime,
261
+ isPlaying
262
+ }) {
263
+ const [showIndicator, setShowIndicator] = useState(false);
264
+ const [isPersisting, setIsPersisting] = useState(false);
265
+ const [isAnimatingOut, setIsAnimatingOut] = useState(false);
266
+ const [iconType, setIconType] = useState("play");
267
+ const hideTimeoutRef = useRef(null);
268
+ const lastHandledToggleTimeRef = useRef(0);
269
+ const isPlayingRef = useRef(isPlaying);
270
+ isPlayingRef.current = isPlaying;
271
+ const persistWhenPausedRef = useRef(persistWhenPaused);
272
+ persistWhenPausedRef.current = persistWhenPaused;
273
+ const prevIsPlayingRef = useRef(isPlaying);
274
+ const clearHideTimeout = () => {
275
+ if (hideTimeoutRef.current) {
276
+ clearTimeout(hideTimeoutRef.current);
277
+ hideTimeoutRef.current = null;
278
+ }
279
+ };
280
+ useEffect(() => {
281
+ const wasPlaying = prevIsPlayingRef.current;
282
+ prevIsPlayingRef.current = isPlaying;
283
+ if (!wasPlaying && isPlaying && isPersisting) {
284
+ const timeSinceLastToggle = Date.now() - lastUserToggleTime;
285
+ const isAutoResume = timeSinceLastToggle > 100;
286
+ if (isAutoResume) {
287
+ if (hideTimeoutRef.current) {
288
+ clearTimeout(hideTimeoutRef.current);
289
+ hideTimeoutRef.current = null;
290
+ }
291
+ setShowIndicator(false);
292
+ setIsPersisting(false);
293
+ setIsAnimatingOut(false);
294
+ }
295
+ }
296
+ }, [isPlaying, isPersisting, lastUserToggleTime]);
297
+ useEffect(() => {
298
+ if (lastUserToggleTime === 0 || lastUserToggleTime === lastHandledToggleTimeRef.current) {
299
+ return;
300
+ }
301
+ lastHandledToggleTimeRef.current = lastUserToggleTime;
302
+ if (isPersisting && isPlaying) {
303
+ setIconType("pause");
304
+ setIsPersisting(false);
305
+ setIsAnimatingOut(true);
306
+ clearHideTimeout();
307
+ hideTimeoutRef.current = setTimeout(() => {
308
+ setShowIndicator(false);
309
+ setIsAnimatingOut(false);
310
+ hideTimeoutRef.current = null;
311
+ }, duration);
312
+ return;
313
+ }
314
+ setShowIndicator(true);
315
+ setIsPersisting(false);
316
+ setIsAnimatingOut(false);
317
+ setIconType("play");
318
+ clearHideTimeout();
319
+ hideTimeoutRef.current = setTimeout(() => {
320
+ const currentIsPlaying = isPlayingRef.current;
321
+ const shouldPersist = persistWhenPausedRef.current;
322
+ if (shouldPersist && !currentIsPlaying) {
323
+ setIsPersisting(true);
324
+ return;
325
+ }
326
+ setShowIndicator(false);
327
+ hideTimeoutRef.current = null;
328
+ }, duration);
329
+ return clearHideTimeout;
330
+ }, [lastUserToggleTime, duration, isPersisting, isPlaying]);
331
+ if (!showIndicator && !isPersisting) return null;
332
+ const iconToShow = iconType === "pause" ? pauseIcon ?? /* @__PURE__ */ jsx(DefaultPauseIcon, { size }) : playIcon ?? /* @__PURE__ */ jsx(DefaultPlayIcon, { size });
333
+ const indicatorClass = [
334
+ "sv-video-slot__play-indicator",
335
+ showIndicator && "sv-video-slot__play-indicator--visible",
336
+ isPersisting && "sv-video-slot__play-indicator--persist",
337
+ isAnimatingOut && "sv-video-slot__play-indicator--animating-out",
338
+ className
339
+ ].filter(Boolean).join(" ");
340
+ return /* @__PURE__ */ jsx(
341
+ "div",
342
+ {
343
+ className: indicatorClass,
344
+ style: { "--sv-indicator-duration": `${duration}ms` },
345
+ "aria-hidden": "true",
346
+ "data-testid": testId,
347
+ children: /* @__PURE__ */ jsx("span", { className: "sv-video-slot__play-indicator-icon", children: iconToShow })
348
+ }
349
+ );
350
+ });
351
+ VideoSlotPlayIndicatorInner.displayName = "VideoSlotPlayIndicatorInner";
352
+ function VideoSlotPlayIndicator(props) {
353
+ const context = useOptionalVideoSlotContext();
354
+ if (!context) return null;
355
+ const lastUserToggleTime = context.lastUserToggleTime ?? 0;
356
+ const isPlaying = context.playerState?.isPlaying ?? false;
357
+ return /* @__PURE__ */ jsx(
358
+ VideoSlotPlayIndicatorInner,
359
+ {
360
+ ...props,
361
+ lastUserToggleTime,
362
+ isPlaying
363
+ }
364
+ );
365
+ }
366
+ VideoSlotPlayIndicator.displayName = "VideoSlotPlayIndicator";
367
+
368
+ export { VideoSlotContext, VideoSlotPlayIndicator, VideoSlotPlayIndicatorInner, useDoubleTap, useOptionalVideoSlotContext, useVideoSlotContext, useVideoSlotIsActive, useVideoSlotIsPlaying, useVideoSlotPlayer, useVideoSlotResource, useVideoSlotVideo };
@@ -1,6 +1,6 @@
1
- import { cn } from './chunk-WKX2WBVO.js';
2
- import { injectComponentCSS } from './chunk-UXMA4KJZ.js';
3
- import { useRef, useCallback, useEffect, useState, useInsertionEffect, useMemo } from 'react';
1
+ import { clsx2 } from './chunk-EDWS2IPH.js';
2
+ import { injectComponentCSS } from './chunk-CAWE42LH.js';
3
+ import { memo, useInsertionEffect, useCallback, useMemo, useRef, useState, useEffect } from 'react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  // src/components/VideoPlayer/VideoPlayer.css.ts
@@ -35,6 +35,7 @@ var VIDEO_PLAYER_CSS = (
35
35
  align-items: center;
36
36
  justify-content: center;
37
37
  z-index: var(--sv-player-video-z);
38
+ background: var(--sv-bg-primary, #000);
38
39
  }
39
40
 
40
41
  /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -653,6 +654,7 @@ function useVideoElement(config) {
653
654
  } = config;
654
655
  const videoRef = useRef(null);
655
656
  const hlsRef = useRef(null);
657
+ const prevTimeRef = useRef(0);
656
658
  const [isLoading, setIsLoading] = useState(true);
657
659
  const [isBuffering, setIsBuffering] = useState(false);
658
660
  const [isReady, setIsReady] = useState(false);
@@ -752,7 +754,15 @@ function useVideoElement(config) {
752
754
  onEnded?.();
753
755
  };
754
756
  const handleTimeUpdate = () => {
755
- onTimeUpdate?.(video.currentTime);
757
+ const currentTime = video.currentTime;
758
+ const duration = video.duration;
759
+ const prevTime = prevTimeRef.current;
760
+ if (loop && duration > 0 && prevTime > duration * 0.9 && // Was near end (>90%)
761
+ currentTime < duration * 0.1) {
762
+ onEnded?.();
763
+ }
764
+ prevTimeRef.current = currentTime;
765
+ onTimeUpdate?.(currentTime);
756
766
  };
757
767
  const handleDurationChange = () => {
758
768
  onDurationChange?.(video.duration);
@@ -819,6 +829,7 @@ function useVideoElement(config) {
819
829
  };
820
830
  }, [
821
831
  autoPlay,
832
+ loop,
822
833
  onCanPlay,
823
834
  onPlay,
824
835
  onPlaying,
@@ -920,7 +931,7 @@ function getMediaErrorMessage(code) {
920
931
  return "Unknown video error";
921
932
  }
922
933
  }
923
- function VideoPlayerHeadless({
934
+ function VideoPlayerHeadlessBase({
924
935
  src,
925
936
  type,
926
937
  poster,
@@ -1021,7 +1032,7 @@ function VideoPlayerHeadless({
1021
1032
  return /* @__PURE__ */ jsxs(
1022
1033
  "div",
1023
1034
  {
1024
- className: cn(PLAYER_CLASS, stateClasses, className),
1035
+ className: clsx2(PLAYER_CLASS, stateClasses, className),
1025
1036
  ...{ [VIDEO_TYPE_ATTR]: type },
1026
1037
  ...{ [PLAYBACK_STATE_ATTR]: playbackState },
1027
1038
  style: {
@@ -1118,6 +1129,7 @@ function getUserFriendlyErrorMessage(message) {
1118
1129
  }
1119
1130
  return "Failed to load video. Please try again.";
1120
1131
  }
1132
+ var VideoPlayerHeadless = memo(VideoPlayerHeadlessBase);
1121
1133
  VideoPlayerHeadless.displayName = "VideoPlayerHeadless";
1122
1134
 
1123
1135
  export { BUFFERING_STATE_CLASS, DEFAULT_OBJECT_FIT, DEFAULT_PRELOAD, ENDED_CLASS, ERROR_CLASS, ERROR_STATE_CLASS, FIRST_FRAME_MAX_WIDTH, FIRST_FRAME_QUALITY, LOADING_CLASS, LOADING_STATE_ATTR, LOADING_STATE_CLASS, PAUSED_CLASS, PLAYBACK_STATE_ATTR, PLAYER_CLASS, PLAYING_CLASS, POSTER_CLASS, READY_CLASS, VIDEO_CLASS, VIDEO_PLAYER_CSS, VIDEO_TYPE_ATTR, VIDEO_WRAPPER_CLASS, VideoElementError, VideoPlayerHeadless, Z_INDEX, Z_INDEX_CSS_VARS, createFirstFrameCache, firstFrameCache, useAutoFirstFrameCapture, useFirstFrameCapture, useVideoElement };