@therealtinhtute/baroiplayer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/dist/context/media.js +2 -0
  4. package/dist/context/media.js.map +1 -0
  5. package/dist/context/media.mjs +15 -0
  6. package/dist/context/media.mjs.map +1 -0
  7. package/dist/context/store.js +2 -0
  8. package/dist/context/store.js.map +1 -0
  9. package/dist/context/store.mjs +5 -0
  10. package/dist/context/store.mjs.map +1 -0
  11. package/dist/context-BMteAJDN.js +1140 -0
  12. package/dist/context-BMteAJDN.js.map +1 -0
  13. package/dist/context-DCJ_scmO.cjs +23 -0
  14. package/dist/context-DCJ_scmO.cjs.map +1 -0
  15. package/dist/core-events-B5W_j8yU.js +480 -0
  16. package/dist/core-events-B5W_j8yU.js.map +1 -0
  17. package/dist/core-events-CN0-Vhhq.cjs +2 -0
  18. package/dist/core-events-CN0-Vhhq.cjs.map +1 -0
  19. package/dist/core-plugins-Bq9KLany.js +696 -0
  20. package/dist/core-plugins-Bq9KLany.js.map +1 -0
  21. package/dist/core-plugins-CPDSHMiQ.cjs +2 -0
  22. package/dist/core-plugins-CPDSHMiQ.cjs.map +1 -0
  23. package/dist/core.js +37 -0
  24. package/dist/core.js.map +1 -0
  25. package/dist/core.mjs +1217 -0
  26. package/dist/core.mjs.map +1 -0
  27. package/dist/enhanced-components-DHv0MFvG.js +1073 -0
  28. package/dist/enhanced-components-DHv0MFvG.js.map +1 -0
  29. package/dist/enhanced-components-DTIObgB7.cjs +2 -0
  30. package/dist/enhanced-components-DTIObgB7.cjs.map +1 -0
  31. package/dist/hooks-C_r8wexn.cjs +4 -0
  32. package/dist/hooks-C_r8wexn.cjs.map +1 -0
  33. package/dist/hooks-Da2wVXlF.js +1630 -0
  34. package/dist/hooks-Da2wVXlF.js.map +1 -0
  35. package/dist/index.css +1 -0
  36. package/dist/index.js +112 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +6126 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/minimal-components-DSq5sQpy.js +2002 -0
  41. package/dist/minimal-components-DSq5sQpy.js.map +1 -0
  42. package/dist/minimal-components-DflWiihN.cjs +2 -0
  43. package/dist/minimal-components-DflWiihN.cjs.map +1 -0
  44. package/dist/minimal-components.css +1 -0
  45. package/dist/mona-loading.gif +0 -0
  46. package/dist/performance-monitor-CUW6f5ll.cjs +2 -0
  47. package/dist/performance-monitor-CUW6f5ll.cjs.map +1 -0
  48. package/dist/performance-monitor-rMW1RgV3.js +302 -0
  49. package/dist/performance-monitor-rMW1RgV3.js.map +1 -0
  50. package/dist/utils-BnscpYYd.js +2798 -0
  51. package/dist/utils-BnscpYYd.js.map +1 -0
  52. package/dist/utils-D_czS_0K.cjs +9 -0
  53. package/dist/utils-D_czS_0K.cjs.map +1 -0
  54. package/dist/utils.js +2 -0
  55. package/dist/utils.js.map +1 -0
  56. package/dist/utils.mjs +7 -0
  57. package/dist/utils.mjs.map +1 -0
  58. package/package.json +114 -0
  59. package/src/adapters/videojs-adapter.tsx +470 -0
  60. package/src/assets/mona-loading.gif +0 -0
  61. package/src/components/AnimateLoading.tsx +115 -0
  62. package/src/components/LoadingState.tsx +248 -0
  63. package/src/components/accessibility/keyboard-help-overlay.tsx +213 -0
  64. package/src/components/audio-player.tsx +85 -0
  65. package/src/components/basic/index.ts +37 -0
  66. package/src/components/enhanced/advanced-features.tsx +316 -0
  67. package/src/components/enhanced/debug-panel.tsx +422 -0
  68. package/src/components/enhanced/enhanced-controls.tsx +600 -0
  69. package/src/components/enhanced/enhanced-media-player.tsx +509 -0
  70. package/src/components/enhanced/error-handling.tsx +345 -0
  71. package/src/components/enhanced/index.ts +39 -0
  72. package/src/components/enhanced/media-player-enhanced.tsx +349 -0
  73. package/src/components/examples/basic-video-player.tsx +55 -0
  74. package/src/components/examples/custom-controls-example.tsx +63 -0
  75. package/src/components/examples/enhanced-controls-example.tsx +114 -0
  76. package/src/components/examples/enhanced-media-player-example.tsx +293 -0
  77. package/src/components/examples/loading-states-demo.tsx +142 -0
  78. package/src/components/examples/react-19-demo.tsx +381 -0
  79. package/src/components/examples/volume-orientation-demo.tsx +114 -0
  80. package/src/components/icon-demo.tsx +209 -0
  81. package/src/components/icons/index.tsx +166 -0
  82. package/src/components/icons/svg-icons/ArrowLeftIcon.tsx +18 -0
  83. package/src/components/icons/svg-icons/ArrowRightIcon.tsx +18 -0
  84. package/src/components/icons/svg-icons/AudioIcon.tsx +22 -0
  85. package/src/components/icons/svg-icons/BackwardIcon.tsx +12 -0
  86. package/src/components/icons/svg-icons/CameraIcon.tsx +19 -0
  87. package/src/components/icons/svg-icons/CheckIcon.tsx +19 -0
  88. package/src/components/icons/svg-icons/EnablePiPIcon.tsx +17 -0
  89. package/src/components/icons/svg-icons/ForwardIcon.tsx +12 -0
  90. package/src/components/icons/svg-icons/FullscreenEnterIcon.tsx +23 -0
  91. package/src/components/icons/svg-icons/FullscreenExitIcon.tsx +23 -0
  92. package/src/components/icons/svg-icons/LoadingIcon.tsx +32 -0
  93. package/src/components/icons/svg-icons/PauseIcon.tsx +23 -0
  94. package/src/components/icons/svg-icons/PlayIcon.tsx +21 -0
  95. package/src/components/icons/svg-icons/PlaybackSpeedIcon.tsx +23 -0
  96. package/src/components/icons/svg-icons/QualityIcon.tsx +21 -0
  97. package/src/components/icons/svg-icons/SettingsIcon.tsx +12 -0
  98. package/src/components/icons/svg-icons/SliderIcon.tsx +21 -0
  99. package/src/components/icons/svg-icons/SubtitleIcon.tsx +21 -0
  100. package/src/components/icons/svg-icons/SubtitleOffIcon.tsx +25 -0
  101. package/src/components/icons/svg-icons/UnPiPIcon.tsx +17 -0
  102. package/src/components/icons/svg-icons/VolumeMutedIcon.tsx +16 -0
  103. package/src/components/icons/svg-icons/VolumeOneIcon.tsx +22 -0
  104. package/src/components/icons/svg-icons/VolumeThreeIcon.tsx +16 -0
  105. package/src/components/icons/svg-icons/VolumeTwoIcon.tsx +16 -0
  106. package/src/components/legacy.tsx +93 -0
  107. package/src/components/media-metadata-display.tsx +100 -0
  108. package/src/components/media-player-audio.tsx +39 -0
  109. package/src/components/media-player-controls.tsx +226 -0
  110. package/src/components/media-player-error.tsx +163 -0
  111. package/src/components/media-player-fullscreen.tsx +109 -0
  112. package/src/components/media-player-hls.tsx +111 -0
  113. package/src/components/media-player-loading.tsx +76 -0
  114. package/src/components/media-player-play.tsx +75 -0
  115. package/src/components/media-player-root.tsx +340 -0
  116. package/src/components/media-player-seek.tsx +168 -0
  117. package/src/components/media-player-time.tsx +99 -0
  118. package/src/components/media-player-tooltip.tsx +58 -0
  119. package/src/components/media-player-video.tsx +39 -0
  120. package/src/components/media-player-volume.tsx +155 -0
  121. package/src/components/media-player.tsx +257 -0
  122. package/src/components/media-suspense.tsx +362 -0
  123. package/src/components/player-settings-form.tsx +163 -0
  124. package/src/components/react-player-wrapper.tsx +651 -0
  125. package/src/components/simple-video-player.tsx +60 -0
  126. package/src/components/ui/enhanced-seek-bar.tsx +235 -0
  127. package/src/components/ui/fullscreen-button.tsx +60 -0
  128. package/src/components/ui/media-resource-preloader.tsx +395 -0
  129. package/src/components/ui/play-button.tsx +107 -0
  130. package/src/components/ui/seek-bar.tsx +161 -0
  131. package/src/components/ui/time-display.tsx +40 -0
  132. package/src/components/ui/volume-control.tsx +143 -0
  133. package/src/components/unified/base-media-player.tsx +246 -0
  134. package/src/components/unified/media-player.tsx +75 -0
  135. package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +561 -0
  136. package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +552 -0
  137. package/src/components/variants/minimal/MinimalControls.tsx +142 -0
  138. package/src/components/variants/minimal/MinimalLoading.tsx +32 -0
  139. package/src/components/variants/minimal/MinimalMediaPlayer.tsx +315 -0
  140. package/src/components/variants/minimal/MinimalOverlay.tsx +64 -0
  141. package/src/components/variants/minimal/MinimalPlayButton.tsx +65 -0
  142. package/src/components/variants/minimal/MinimalSeekBar.tsx +150 -0
  143. package/src/components/variants/minimal/MinimalVolumeControl.tsx +80 -0
  144. package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +210 -0
  145. package/src/components/variants/minimal/components/CenterPlayButton.tsx +22 -0
  146. package/src/components/variants/minimal/components/ControlBar.tsx +146 -0
  147. package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +361 -0
  148. package/src/components/variants/minimal/components/PlaybackControls.tsx +51 -0
  149. package/src/components/variants/minimal/components/RightControls.tsx +173 -0
  150. package/src/components/variants/minimal/components/SeekBar.tsx +131 -0
  151. package/src/components/variants/minimal/components/SettingsDropdown.tsx +70 -0
  152. package/src/components/variants/minimal/components/SubtitleDisplay.tsx +21 -0
  153. package/src/components/variants/minimal/components/TimeDisplay.tsx +18 -0
  154. package/src/components/variants/minimal/components/VideoElement.tsx +40 -0
  155. package/src/components/variants/minimal/components/VolumeControl.tsx +217 -0
  156. package/src/components/variants/minimal/index.ts +23 -0
  157. package/src/components/video-player.tsx +100 -0
  158. package/src/context/error-handling.ts +394 -0
  159. package/src/context/media-context.tsx +208 -0
  160. package/src/context/media-provider.tsx +265 -0
  161. package/src/context/media-store.ts +379 -0
  162. package/src/context/persistence.ts +453 -0
  163. package/src/core/events/event-system.ts +442 -0
  164. package/src/core/events/hooks.ts +302 -0
  165. package/src/core/events/index.ts +22 -0
  166. package/src/core/events/middleware.ts +269 -0
  167. package/src/core/index.ts +22 -0
  168. package/src/core/optimization/bundle-optimizer.tsx +308 -0
  169. package/src/core/performance/cicd.ts +96 -0
  170. package/src/core/performance/index.ts +38 -0
  171. package/src/core/performance/media-loading-optimizer.ts +538 -0
  172. package/src/core/performance/performance-budgets.ts +432 -0
  173. package/src/core/performance/performance-monitor.tsx +476 -0
  174. package/src/core/performance/performance-testing.ts +459 -0
  175. package/src/core/plugins/base-plugin.ts +224 -0
  176. package/src/core/plugins/enhanced-plugin-system.ts +411 -0
  177. package/src/core/plugins/examples/analytics-plugin.ts +242 -0
  178. package/src/core/plugins/index.ts +39 -0
  179. package/src/core/plugins/registry.ts +327 -0
  180. package/src/core/plugins/types.ts +159 -0
  181. package/src/core/plugins/utils.ts +316 -0
  182. package/src/core/progressive-loading.tsx +322 -0
  183. package/src/core/providers/enhanced-hls-provider.ts +339 -0
  184. package/src/core/providers/service-provider.tsx +168 -0
  185. package/src/core/utils.tsx +38 -0
  186. package/src/features/media-session/media-session-manager.ts +429 -0
  187. package/src/features/picture-in-picture/pip-manager.ts +429 -0
  188. package/src/features/playlist/playlist-manager.ts +606 -0
  189. package/src/features/subtitles/subtitle-manager.ts +441 -0
  190. package/src/features/subtitles/subtitle-parser.ts +280 -0
  191. package/src/features/thumbnails/thumbnail-generator.ts +519 -0
  192. package/src/hooks/accessibility/index.ts +27 -0
  193. package/src/hooks/accessibility/use-accessibility-preferences.ts +188 -0
  194. package/src/hooks/accessibility/use-focus-management.ts +176 -0
  195. package/src/hooks/accessibility/use-keyboard-shortcuts.ts +144 -0
  196. package/src/hooks/accessibility/use-screen-reader.ts +182 -0
  197. package/src/hooks/core/index.ts +47 -0
  198. package/src/hooks/legacy.ts +27 -0
  199. package/src/hooks/use-enhanced-media-player.ts +468 -0
  200. package/src/hooks/use-intelligent-preloading.ts +358 -0
  201. package/src/hooks/use-keyboard-controls.ts +115 -0
  202. package/src/hooks/use-media-actions.ts +175 -0
  203. package/src/hooks/use-media-metadata.ts +89 -0
  204. package/src/hooks/use-media-optimistic.ts +270 -0
  205. package/src/hooks/use-media-player.ts +312 -0
  206. package/src/hooks/use-media-state.ts +190 -0
  207. package/src/hooks/use-memory-optimization.ts +358 -0
  208. package/src/hooks/use-optimistic-controls.ts +92 -0
  209. package/src/hooks/use-optimized-media.ts +263 -0
  210. package/src/hooks/use-touch-gestures.ts +417 -0
  211. package/src/hooks/useEnhancedSubtitles.ts +258 -0
  212. package/src/hooks/useKeyboardShortcuts.ts +74 -0
  213. package/src/hooks/useMediaControls.ts +52 -0
  214. package/src/hooks/useScreenshot.ts +57 -0
  215. package/src/hooks/useSubtitles.ts +91 -0
  216. package/src/hooks/useVideoPlayer.ts +232 -0
  217. package/src/icons/components/FullscreenIcon.tsx +37 -0
  218. package/src/icons/components/SettingsIcon.tsx +38 -0
  219. package/src/icons/components/animated/LoadingIcon.tsx +43 -0
  220. package/src/icons/components/animated/PlayPauseIcon.tsx +50 -0
  221. package/src/icons/components/media/PauseIcon.tsx +39 -0
  222. package/src/icons/components/media/PlayIcon.tsx +38 -0
  223. package/src/icons/components/volume/VolumeIcon.tsx +39 -0
  224. package/src/icons/components/volume/VolumeOffIcon.tsx +39 -0
  225. package/src/icons/index.ts +56 -0
  226. package/src/icons/provider.tsx +64 -0
  227. package/src/icons/types.ts +40 -0
  228. package/src/index-enhanced.ts +35 -0
  229. package/src/index-legacy.ts +326 -0
  230. package/src/index.css +102 -0
  231. package/src/index.ts +216 -0
  232. package/src/lib/deprecation.ts +51 -0
  233. package/src/lib/format-time.ts +37 -0
  234. package/src/lib/index.ts +3 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/globals.css +462 -0
  237. package/src/styles/minimal-player.css +589 -0
  238. package/src/styles/pixel-art.css +46 -0
  239. package/src/styles/videojs.css +100 -0
  240. package/src/test/setup.ts +79 -0
  241. package/src/types/events.ts +47 -0
  242. package/src/types/formats.ts +95 -0
  243. package/src/types/index.ts +190 -0
  244. package/src/types/videojs-plugins.d.ts +14 -0
@@ -0,0 +1,263 @@
1
+ "use client"
2
+
3
+ import React from "react"
4
+ import { useMediaSelector, useMediaDispatch, MediaActionTypes } from "../context/media-context"
5
+
6
+ // Optimized hook for media state with memoized selectors
7
+ export function useOptimizedMediaState() {
8
+ // Memoized selectors to prevent unnecessary re-renders
9
+ const isPlaying = useMediaSelector(React.useCallback(state => state.isPlaying, []))
10
+ const currentTime = useMediaSelector(React.useCallback(state => state.currentTime, []))
11
+ const duration = useMediaSelector(React.useCallback(state => state.duration, []))
12
+ const volume = useMediaSelector(React.useCallback(state => state.volume, []))
13
+ const isMuted = useMediaSelector(React.useCallback(state => state.isMuted, []))
14
+ const isLoading = useMediaSelector(React.useCallback(state => state.isLoading, []))
15
+ const error = useMediaSelector(React.useCallback(state => state.error, []))
16
+ const isFullscreen = useMediaSelector(React.useCallback(state => state.isFullscreen, []))
17
+ const isPictureInPicture = useMediaSelector(React.useCallback(state => state.isPictureInPicture, []))
18
+ const buffered = useMediaSelector(React.useCallback(state => state.buffered, []))
19
+ const playbackRate = useMediaSelector(React.useCallback(state => state.playbackRate, []))
20
+
21
+ // Derived state with memoization
22
+ const progress = React.useMemo(() => {
23
+ return duration > 0 ? (currentTime / duration) * 100 : 0
24
+ }, [currentTime, duration])
25
+
26
+ const remainingTime = React.useMemo(() => {
27
+ return Math.max(0, duration - currentTime)
28
+ }, [duration, currentTime])
29
+
30
+ const bufferedRanges = React.useMemo(() => {
31
+ if (!buffered || duration === 0) return []
32
+ const ranges = []
33
+ for (let i = 0; i < buffered.length; i++) {
34
+ ranges.push({
35
+ start: (buffered.start(i) / duration) * 100,
36
+ end: (buffered.end(i) / duration) * 100
37
+ })
38
+ }
39
+ return ranges
40
+ }, [buffered, duration])
41
+
42
+ const canPlay = React.useMemo(() => {
43
+ return !isLoading && !error && duration > 0
44
+ }, [isLoading, error, duration])
45
+
46
+ return {
47
+ // Basic state
48
+ isPlaying,
49
+ currentTime,
50
+ duration,
51
+ volume,
52
+ isMuted,
53
+ isLoading,
54
+ error,
55
+ isFullscreen,
56
+ isPictureInPicture,
57
+ buffered,
58
+ playbackRate,
59
+
60
+ // Derived state
61
+ progress,
62
+ remainingTime,
63
+ bufferedRanges,
64
+ canPlay
65
+ }
66
+ }
67
+
68
+ // Optimized hook for media controls with memoized callbacks
69
+ export function useOptimizedMediaControls() {
70
+ const dispatch = useMediaDispatch()
71
+
72
+ // Memoized control functions
73
+ const play = React.useCallback(() => {
74
+ dispatch({ type: MediaActionTypes.PLAY })
75
+ }, [dispatch])
76
+
77
+ const pause = React.useCallback(() => {
78
+ dispatch({ type: MediaActionTypes.PAUSE })
79
+ }, [dispatch])
80
+
81
+ const toggle = React.useCallback(() => {
82
+ dispatch({ type: MediaActionTypes.PLAY })
83
+ }, [dispatch])
84
+
85
+ const seek = React.useCallback((time: number) => {
86
+ dispatch({ type: MediaActionTypes.SEEK, detail: time })
87
+ }, [dispatch])
88
+
89
+ const setVolume = React.useCallback((volume: number) => {
90
+ dispatch({ type: MediaActionTypes.SET_VOLUME, detail: volume })
91
+ }, [dispatch])
92
+
93
+ const mute = React.useCallback(() => {
94
+ dispatch({ type: MediaActionTypes.MUTE })
95
+ }, [dispatch])
96
+
97
+ const unmute = React.useCallback(() => {
98
+ dispatch({ type: MediaActionTypes.UNMUTE })
99
+ }, [dispatch])
100
+
101
+ const toggleMute = React.useCallback(() => {
102
+ dispatch({ type: MediaActionTypes.TOGGLE_MUTE })
103
+ }, [dispatch])
104
+
105
+ const enterFullscreen = React.useCallback(() => {
106
+ dispatch({ type: MediaActionTypes.ENTER_FULLSCREEN })
107
+ }, [dispatch])
108
+
109
+ const exitFullscreen = React.useCallback(() => {
110
+ dispatch({ type: MediaActionTypes.EXIT_FULLSCREEN })
111
+ }, [dispatch])
112
+
113
+ const toggleFullscreen = React.useCallback(() => {
114
+ dispatch({ type: MediaActionTypes.ENTER_FULLSCREEN })
115
+ }, [dispatch])
116
+
117
+ const enterPictureInPicture = React.useCallback(() => {
118
+ dispatch({ type: MediaActionTypes.ENTER_PIP })
119
+ }, [dispatch])
120
+
121
+ const exitPictureInPicture = React.useCallback(() => {
122
+ dispatch({ type: MediaActionTypes.EXIT_PIP })
123
+ }, [dispatch])
124
+
125
+ const togglePictureInPicture = React.useCallback(() => {
126
+ dispatch({ type: MediaActionTypes.ENTER_PIP })
127
+ }, [dispatch])
128
+
129
+ const setPlaybackRate = React.useCallback((rate: number) => {
130
+ dispatch({ type: MediaActionTypes.SET_PLAYBACK_RATE, detail: rate })
131
+ }, [dispatch])
132
+
133
+ const skipForward = React.useCallback((seconds: number = 10) => {
134
+ dispatch({ type: MediaActionTypes.SEEK, detail: seconds })
135
+ }, [dispatch])
136
+
137
+ const skipBackward = React.useCallback((seconds: number = 10) => {
138
+ dispatch({ type: MediaActionTypes.SEEK, detail: -seconds })
139
+ }, [dispatch])
140
+
141
+ return {
142
+ play,
143
+ pause,
144
+ toggle,
145
+ seek,
146
+ setVolume,
147
+ mute,
148
+ unmute,
149
+ toggleMute,
150
+ enterFullscreen,
151
+ exitFullscreen,
152
+ toggleFullscreen,
153
+ enterPictureInPicture,
154
+ exitPictureInPicture,
155
+ togglePictureInPicture,
156
+ setPlaybackRate,
157
+ skipForward,
158
+ skipBackward
159
+ }
160
+ }
161
+
162
+ // Hook for debounced seek operations
163
+ export function useDebouncedSeek(delay: number = 100) {
164
+ const dispatch = useMediaDispatch()
165
+ const timeoutRef = React.useRef<NodeJS.Timeout | null>(null)
166
+
167
+ const debouncedSeek = React.useCallback((time: number) => {
168
+ if (timeoutRef.current) {
169
+ clearTimeout(timeoutRef.current)
170
+ }
171
+
172
+ timeoutRef.current = setTimeout(() => {
173
+ dispatch({ type: MediaActionTypes.SEEK, detail: time })
174
+ }, delay)
175
+ }, [dispatch, delay])
176
+
177
+ React.useEffect(() => {
178
+ return () => {
179
+ if (timeoutRef.current) {
180
+ clearTimeout(timeoutRef.current)
181
+ }
182
+ }
183
+ }, [])
184
+
185
+ return debouncedSeek
186
+ }
187
+
188
+ // Hook for throttled volume changes
189
+ export function useThrottledVolume(delay: number = 50) {
190
+ const dispatch = useMediaDispatch()
191
+ const lastUpdateRef = React.useRef<number>(0)
192
+
193
+ const throttledSetVolume = React.useCallback((volume: number) => {
194
+ const now = Date.now()
195
+ if (now - lastUpdateRef.current >= delay) {
196
+ dispatch({ type: MediaActionTypes.SET_VOLUME, detail: volume })
197
+ lastUpdateRef.current = now
198
+ }
199
+ }, [dispatch, delay])
200
+
201
+ return throttledSetVolume
202
+ }
203
+
204
+ // Hook for optimized time formatting
205
+ export function useFormattedTime(time: number) {
206
+ return React.useMemo(() => {
207
+ const hours = Math.floor(time / 3600)
208
+ const minutes = Math.floor((time % 3600) / 60)
209
+ const seconds = Math.floor(time % 60)
210
+
211
+ if (hours > 0) {
212
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
213
+ }
214
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`
215
+ }, [time])
216
+ }
217
+
218
+ // Hook for intersection observer (for lazy loading)
219
+ export function useIntersectionObserver(
220
+ elementRef: React.RefObject<Element>,
221
+ options?: IntersectionObserverInit
222
+ ) {
223
+ const [isIntersecting, setIsIntersecting] = React.useState(false)
224
+
225
+ React.useEffect(() => {
226
+ const element = elementRef.current
227
+ if (!element) return
228
+
229
+ const observer = new IntersectionObserver(
230
+ ([entry]) => {
231
+ setIsIntersecting(entry?.isIntersecting ?? false)
232
+ },
233
+ options
234
+ )
235
+
236
+ observer.observe(element)
237
+
238
+ return () => {
239
+ observer.unobserve(element)
240
+ }
241
+ }, [elementRef, options])
242
+
243
+ return isIntersecting
244
+ }
245
+
246
+ // Hook for media queries (responsive behavior)
247
+ export function useMediaQuery(query: string) {
248
+ const [matches, setMatches] = React.useState(false)
249
+
250
+ React.useEffect(() => {
251
+ const mediaQuery = window.matchMedia(query)
252
+ setMatches(mediaQuery.matches)
253
+
254
+ const handler = (event: MediaQueryListEvent) => {
255
+ setMatches(event.matches)
256
+ }
257
+
258
+ mediaQuery.addEventListener('change', handler)
259
+ return () => mediaQuery.removeEventListener('change', handler)
260
+ }, [query])
261
+
262
+ return matches
263
+ }
@@ -0,0 +1,417 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useDeviceInfo, useHapticFeedback } from "@rp/ui/hooks/use-mobile"
5
+
6
+ // Gesture types
7
+ export type GestureType =
8
+ | 'tap'
9
+ | 'double-tap'
10
+ | 'long-press'
11
+ | 'swipe-left'
12
+ | 'swipe-right'
13
+ | 'swipe-up'
14
+ | 'swipe-down'
15
+ | 'pinch-in'
16
+ | 'pinch-out'
17
+
18
+ // Gesture event data
19
+ export interface GestureEvent {
20
+ type: GestureType
21
+ startPoint: { x: number; y: number }
22
+ currentPoint: { x: number; y: number }
23
+ velocity?: { x: number; y: number }
24
+ scale?: number
25
+ rotation?: number
26
+ distance?: number
27
+ duration: number
28
+ preventDefault: () => void
29
+ target: HTMLElement
30
+ }
31
+
32
+ // Gesture configuration
33
+ export interface GestureConfig {
34
+ enabledGestures?: GestureType[]
35
+ tapThreshold?: number // ms
36
+ doubleTapThreshold?: number // ms
37
+ longPressThreshold?: number // ms
38
+ swipeThreshold?: number // px
39
+ swipeVelocityThreshold?: number // px/ms
40
+ pinchThreshold?: number // scale difference
41
+ rotationThreshold?: number // degrees
42
+ enableHapticFeedback?: boolean
43
+ }
44
+
45
+ // Default configuration
46
+ const defaultConfig: Required<GestureConfig> = {
47
+ enabledGestures: ['tap', 'double-tap', 'long-press', 'swipe-left', 'swipe-right', 'swipe-up', 'swipe-down', 'pinch-in', 'pinch-out'],
48
+ tapThreshold: 200,
49
+ doubleTapThreshold: 300,
50
+ longPressThreshold: 500,
51
+ swipeThreshold: 50,
52
+ swipeVelocityThreshold: 0.5,
53
+ pinchThreshold: 0.1,
54
+ rotationThreshold: 15,
55
+ enableHapticFeedback: true
56
+ }
57
+
58
+ // Touch point interface
59
+ interface TouchPoint {
60
+ id: number
61
+ x: number
62
+ y: number
63
+ timestamp: number
64
+ }
65
+
66
+ // Gesture state
67
+ interface GestureState {
68
+ isTracking: boolean
69
+ touches: TouchPoint[]
70
+ startTouches: TouchPoint[]
71
+ lastTap?: { timestamp: number; point: { x: number; y: number } }
72
+ longPressTimer?: NodeJS.Timeout
73
+ initialDistance?: number
74
+ initialScale?: number
75
+ lastScale?: number
76
+ }
77
+
78
+ // Touch gesture hook
79
+ export function useTouchGestures(
80
+ ref: React.RefObject<HTMLElement>,
81
+ onGesture: (event: GestureEvent) => void,
82
+ config: GestureConfig = {}
83
+ ) {
84
+ const mergedConfig = { ...defaultConfig, ...config }
85
+ const { isTouchDevice } = useDeviceInfo()
86
+ const { triggerHaptic } = useHapticFeedback()
87
+ const gestureState = React.useRef<GestureState>({
88
+ isTracking: false,
89
+ touches: [],
90
+ startTouches: []
91
+ })
92
+
93
+ // Helper functions
94
+ const getTouchPoint = (touch: Touch): TouchPoint => ({
95
+ id: touch.identifier,
96
+ x: touch.clientX,
97
+ y: touch.clientY,
98
+ timestamp: Date.now()
99
+ })
100
+
101
+ const getDistance = (p1: TouchPoint, p2: TouchPoint): number =>
102
+ Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))
103
+
104
+ const getVelocity = (start: TouchPoint, end: TouchPoint): { x: number; y: number } => {
105
+ const timeDiff = Math.max(end.timestamp - start.timestamp, 1)
106
+ return {
107
+ x: (end.x - start.x) / timeDiff,
108
+ y: (end.y - start.y) / timeDiff
109
+ }
110
+ }
111
+
112
+ const createGestureEvent = (
113
+ type: GestureType,
114
+ startPoint: TouchPoint,
115
+ currentPoint: TouchPoint,
116
+ additionalData: Partial<GestureEvent> = {}
117
+ ): GestureEvent => ({
118
+ type,
119
+ startPoint: { x: startPoint.x, y: startPoint.y },
120
+ currentPoint: { x: currentPoint.x, y: currentPoint.y },
121
+ duration: currentPoint.timestamp - startPoint.timestamp,
122
+ target: ref.current!,
123
+ preventDefault: () => {},
124
+ ...additionalData
125
+ })
126
+
127
+ // Handle touch start
128
+ const handleTouchStart = React.useCallback((e: TouchEvent) => {
129
+ if (!mergedConfig.enabledGestures.length) return
130
+
131
+ const touches = Array.from(e.touches).map(getTouchPoint)
132
+ const state = gestureState.current
133
+
134
+ state.isTracking = true
135
+ state.touches = touches
136
+ state.startTouches = [...touches]
137
+
138
+ // Clear any existing long press timer
139
+ if (state.longPressTimer) {
140
+ clearTimeout(state.longPressTimer)
141
+ }
142
+
143
+ // Set up long press detection for single touch
144
+ if (touches.length === 1 && mergedConfig.enabledGestures.includes('long-press')) {
145
+ state.longPressTimer = setTimeout(() => {
146
+ if (state.touches.length === 1 && touches[0]) {
147
+ const gestureEvent = createGestureEvent('long-press', touches[0], touches[0])
148
+ onGesture(gestureEvent)
149
+
150
+ if (mergedConfig.enableHapticFeedback) {
151
+ triggerHaptic('heavy')
152
+ }
153
+ }
154
+ }, mergedConfig.longPressThreshold)
155
+ }
156
+
157
+ // Handle multi-touch gestures
158
+ if (touches.length === 2 && touches[0] && touches[1]) {
159
+ state.initialDistance = getDistance(touches[0], touches[1])
160
+ state.initialScale = 1
161
+ state.lastScale = 1
162
+ }
163
+ }, [mergedConfig, onGesture, triggerHaptic])
164
+
165
+ // Handle touch move
166
+ const handleTouchMove = React.useCallback((e: TouchEvent) => {
167
+ const state = gestureState.current
168
+ if (!state.isTracking) return
169
+
170
+ const touches = Array.from(e.touches).map(getTouchPoint)
171
+ state.touches = touches
172
+
173
+ // Handle pinch gesture
174
+ if (touches.length === 2 && state.initialDistance && touches[0] && touches[1]) {
175
+ const currentDistance = getDistance(touches[0], touches[1])
176
+ const scale = currentDistance / state.initialDistance
177
+ const scaleDiff = Math.abs(scale - (state.lastScale || 1))
178
+
179
+ if (scaleDiff > mergedConfig.pinchThreshold) {
180
+ const gestureType = scale > (state.lastScale || 1) ? 'pinch-out' : 'pinch-in'
181
+
182
+ if (mergedConfig.enabledGestures.includes(gestureType)) {
183
+ const centerPoint = {
184
+ x: (touches[0].x + touches[1].x) / 2,
185
+ y: (touches[0].y + touches[1].y) / 2,
186
+ id: -1,
187
+ timestamp: Date.now()
188
+ }
189
+
190
+ const startCenterPoint = {
191
+ x: ((state.startTouches[0]?.x ?? 0) + (state.startTouches[1]?.x ?? 0)) / 2,
192
+ y: ((state.startTouches[0]?.y ?? 0) + (state.startTouches[1]?.y ?? 0)) / 2,
193
+ id: -1,
194
+ timestamp: state.startTouches[0]?.timestamp ?? Date.now()
195
+ }
196
+
197
+ const gestureEvent = createGestureEvent(gestureType, startCenterPoint, centerPoint, {
198
+ scale,
199
+ distance: currentDistance
200
+ })
201
+
202
+ onGesture(gestureEvent)
203
+ state.lastScale = scale
204
+ }
205
+ }
206
+ }
207
+
208
+ // Clear long press timer on move
209
+ if (state.longPressTimer && touches.length === 1 && state.startTouches[0] && touches[0]) {
210
+ const startTouch = state.startTouches[0]
211
+ const currentTouch = touches[0]
212
+ const distance = getDistance(startTouch, currentTouch)
213
+
214
+ if (distance > mergedConfig.swipeThreshold) {
215
+ clearTimeout(state.longPressTimer)
216
+ state.longPressTimer = undefined
217
+ }
218
+ }
219
+ }, [mergedConfig, onGesture])
220
+
221
+ // Handle touch end
222
+ const handleTouchEnd = React.useCallback((e: TouchEvent) => {
223
+ const state = gestureState.current
224
+ if (!state.isTracking) return
225
+
226
+ const endTouches = Array.from(e.changedTouches).map(getTouchPoint)
227
+ const currentTime = Date.now()
228
+
229
+ // Clear long press timer
230
+ if (state.longPressTimer) {
231
+ clearTimeout(state.longPressTimer)
232
+ state.longPressTimer = undefined
233
+ }
234
+
235
+ // Handle single touch gestures
236
+ if (state.startTouches.length === 1 && endTouches.length === 1 && state.startTouches[0] && endTouches[0]) {
237
+ const startTouch = state.startTouches[0]
238
+ const endTouch = endTouches[0]
239
+ const distance = getDistance(startTouch, endTouch)
240
+ const duration = endTouch.timestamp - startTouch.timestamp
241
+
242
+ // Check for swipe gestures
243
+ if (distance > mergedConfig.swipeThreshold) {
244
+ const velocity = getVelocity(startTouch, endTouch)
245
+ const absVelX = Math.abs(velocity.x)
246
+ const absVelY = Math.abs(velocity.y)
247
+
248
+ if (absVelX > mergedConfig.swipeVelocityThreshold || absVelY > mergedConfig.swipeVelocityThreshold) {
249
+ let gestureType: GestureType
250
+
251
+ if (absVelX > absVelY) {
252
+ gestureType = velocity.x > 0 ? 'swipe-right' : 'swipe-left'
253
+ } else {
254
+ gestureType = velocity.y > 0 ? 'swipe-down' : 'swipe-up'
255
+ }
256
+
257
+ if (mergedConfig.enabledGestures.includes(gestureType)) {
258
+ const gestureEvent = createGestureEvent(gestureType, startTouch, endTouch, {
259
+ velocity,
260
+ distance
261
+ })
262
+
263
+ onGesture(gestureEvent)
264
+
265
+ if (mergedConfig.enableHapticFeedback) {
266
+ triggerHaptic('light')
267
+ }
268
+ }
269
+ }
270
+ }
271
+ // Check for tap gestures
272
+ else if (duration < mergedConfig.tapThreshold && distance < mergedConfig.swipeThreshold) {
273
+ // Check for double tap
274
+ if (state.lastTap &&
275
+ currentTime - state.lastTap.timestamp < mergedConfig.doubleTapThreshold &&
276
+ getDistance({...state.lastTap.point, id: -1, timestamp: state.lastTap.timestamp}, endTouch) < mergedConfig.swipeThreshold) {
277
+
278
+ if (mergedConfig.enabledGestures.includes('double-tap')) {
279
+ const gestureEvent = createGestureEvent('double-tap', startTouch, endTouch)
280
+ onGesture(gestureEvent)
281
+
282
+ if (mergedConfig.enableHapticFeedback) {
283
+ triggerHaptic('medium')
284
+ }
285
+ }
286
+
287
+ state.lastTap = undefined
288
+ } else {
289
+ // Single tap
290
+ if (mergedConfig.enabledGestures.includes('tap')) {
291
+ const gestureEvent = createGestureEvent('tap', startTouch, endTouch)
292
+ onGesture(gestureEvent)
293
+
294
+ if (mergedConfig.enableHapticFeedback) {
295
+ triggerHaptic('light')
296
+ }
297
+ }
298
+
299
+ state.lastTap = {
300
+ timestamp: currentTime,
301
+ point: { x: endTouch.x, y: endTouch.y }
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ // Reset gesture state
308
+ if (e.touches.length === 0) {
309
+ state.isTracking = false
310
+ state.touches = []
311
+ state.startTouches = []
312
+ state.initialDistance = undefined
313
+ state.initialScale = undefined
314
+ state.lastScale = undefined
315
+ }
316
+ }, [mergedConfig, onGesture, triggerHaptic])
317
+
318
+ // Attach event listeners
319
+ React.useEffect(() => {
320
+ const element = ref.current
321
+ if (!element || !isTouchDevice) return
322
+
323
+ element.addEventListener('touchstart', handleTouchStart, { passive: false })
324
+ element.addEventListener('touchmove', handleTouchMove, { passive: false })
325
+ element.addEventListener('touchend', handleTouchEnd, { passive: false })
326
+ element.addEventListener('touchcancel', handleTouchEnd, { passive: false })
327
+
328
+ return () => {
329
+ element.removeEventListener('touchstart', handleTouchStart)
330
+ element.removeEventListener('touchmove', handleTouchMove)
331
+ element.removeEventListener('touchend', handleTouchEnd)
332
+ element.removeEventListener('touchcancel', handleTouchEnd)
333
+ }
334
+ }, [handleTouchStart, handleTouchMove, handleTouchEnd, isTouchDevice])
335
+
336
+ return {
337
+ isGestureEnabled: isTouchDevice && mergedConfig.enabledGestures.length > 0
338
+ }
339
+ }
340
+
341
+ // Media player specific gesture hook with predefined actions
342
+ export function useMediaGestures(
343
+ ref: React.RefObject<HTMLElement>,
344
+ actions: {
345
+ onPlayPause?: () => void
346
+ onSeek?: (direction: 'forward' | 'backward', amount: number) => void
347
+ onVolumeChange?: (direction: 'up' | 'down', amount: number) => void
348
+ onToggleFullscreen?: () => void
349
+ onZoom?: (scale: number) => void
350
+ }
351
+ ) {
352
+ const handleGesture = React.useCallback((event: GestureEvent) => {
353
+ const { type, velocity, distance, scale } = event
354
+
355
+ switch (type) {
356
+ case 'double-tap':
357
+ // Double tap to play/pause
358
+ actions.onPlayPause?.()
359
+ break
360
+
361
+ case 'swipe-left':
362
+ // Swipe left to seek backward
363
+ const backwardAmount = Math.min((distance || 0) / 10, 30) // Max 30 seconds
364
+ actions.onSeek?.('backward', backwardAmount)
365
+ break
366
+
367
+ case 'swipe-right':
368
+ // Swipe right to seek forward
369
+ const forwardAmount = Math.min((distance || 0) / 10, 30) // Max 30 seconds
370
+ actions.onSeek?.('forward', forwardAmount)
371
+ break
372
+
373
+ case 'swipe-up':
374
+ // Swipe up on right side for volume up
375
+ if (event.startPoint.x > (ref.current?.clientWidth || 0) / 2) {
376
+ const volumeAmount = Math.min((distance || 0) / 100, 0.2) // Max 20%
377
+ actions.onVolumeChange?.('up', volumeAmount)
378
+ }
379
+ break
380
+
381
+ case 'swipe-down':
382
+ // Swipe down on right side for volume down
383
+ if (event.startPoint.x > (ref.current?.clientWidth || 0) / 2) {
384
+ const volumeAmount = Math.min((distance || 0) / 100, 0.2) // Max 20%
385
+ actions.onVolumeChange?.('down', volumeAmount)
386
+ }
387
+ break
388
+
389
+ case 'pinch-out':
390
+ // Pinch out to zoom in (for video zoom functionality)
391
+ actions.onZoom?.(scale || 1)
392
+ break
393
+
394
+ case 'pinch-in':
395
+ // Pinch in to zoom out
396
+ actions.onZoom?.(scale || 1)
397
+ break
398
+
399
+ case 'long-press':
400
+ // Long press for fullscreen toggle
401
+ actions.onToggleFullscreen?.()
402
+ break
403
+
404
+ default:
405
+ break
406
+ }
407
+ }, [actions, ref])
408
+
409
+ const gestureConfig: GestureConfig = {
410
+ enabledGestures: ['tap', 'double-tap', 'long-press', 'swipe-left', 'swipe-right', 'swipe-up', 'swipe-down', 'pinch-in', 'pinch-out'],
411
+ enableHapticFeedback: true,
412
+ swipeThreshold: 30,
413
+ swipeVelocityThreshold: 0.3
414
+ }
415
+
416
+ return useTouchGestures(ref, handleGesture, gestureConfig)
417
+ }