@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,75 @@
1
+ "use client"
2
+
3
+ import { Button } from "@rp/ui/components/button"
4
+ import { PauseIcon, PlayIcon } from "lucide-react"
5
+ import React from "react"
6
+ import {
7
+ MediaActionTypes,
8
+ useMediaContext,
9
+ useMediaDispatch,
10
+ useMediaSelector
11
+ } from "../context/media-context"
12
+ import { MediaPlayerTooltip } from "./media-player-tooltip"
13
+
14
+ export interface MediaPlayerPlayProps {
15
+ className?: string
16
+ asChild?: boolean
17
+ children?: React.ReactNode
18
+ }
19
+
20
+ export function MediaPlayerPlay({
21
+ className,
22
+ asChild = false,
23
+ children,
24
+ ...props
25
+ }: MediaPlayerPlayProps) {
26
+ const context = useMediaContext()
27
+ const dispatch = useMediaDispatch()
28
+ const isPlaying = useMediaSelector((state) => state.isPlaying)
29
+ const disabled = useMediaSelector((state) => state.error !== null)
30
+
31
+ const handleClick = React.useCallback(
32
+ (event: React.MouseEvent) => {
33
+ event.preventDefault()
34
+
35
+ const mediaElement = context.mediaRef.current
36
+ if (!mediaElement || disabled) return
37
+
38
+ dispatch({
39
+ type: isPlaying ? MediaActionTypes.PAUSE : MediaActionTypes.PLAY
40
+ })
41
+ },
42
+ [dispatch, context.mediaRef, isPlaying, disabled]
43
+ )
44
+
45
+ const buttonProps = {
46
+ type: "button" as const,
47
+ "aria-controls": context.mediaId,
48
+ "aria-label": isPlaying ? "Pause" : "Play",
49
+ "aria-pressed": isPlaying,
50
+ "data-disabled": disabled ? "" : undefined,
51
+ "data-slot": "media-player-play",
52
+ "data-state": isPlaying ? "playing" : "paused",
53
+ disabled,
54
+ variant: "ghost" as const,
55
+ size: "icon" as const,
56
+ className: "size-8 [&_svg:not([class*='fill-'])]:fill-current",
57
+ onClick: handleClick,
58
+ ...props
59
+ }
60
+
61
+ const content = isPlaying ? <PauseIcon className="size-4" /> : <PlayIcon className="size-4" />
62
+
63
+ if (asChild) {
64
+ const child = React.Children.only(children) as React.ReactElement
65
+ return React.cloneElement(child, buttonProps as any, content)
66
+ }
67
+
68
+ return (
69
+ <MediaPlayerTooltip tooltip={isPlaying ? "Pause" : "Play"} shortcut="Space">
70
+ <Button {...buttonProps}>{content}</Button>
71
+ </MediaPlayerTooltip>
72
+ )
73
+ }
74
+
75
+ MediaPlayerPlay.displayName = "MediaPlayerPlay"
@@ -0,0 +1,340 @@
1
+ "use client"
2
+
3
+ import React from "react"
4
+ import {
5
+ MediaActionTypes,
6
+ useMediaContext,
7
+ useMediaDispatch,
8
+ useMediaSelector
9
+ } from "../context/media-context"
10
+ import { MediaProvider } from "../context/media-provider"
11
+ import { useMediaGestures } from "../hooks/use-touch-gestures"
12
+ import { useMobileOptimizations, useDeviceInfo } from "@rp/ui/hooks/use-mobile"
13
+ import { cn } from "../lib/utils"
14
+
15
+ // Utility function to compose refs
16
+ function composeRefs<T>(...refs: Array<React.Ref<T> | undefined>) {
17
+ return (node: T) => {
18
+ refs.forEach((ref) => {
19
+ if (typeof ref === "function") {
20
+ ref(node)
21
+ } else if (ref && typeof ref === "object") {
22
+ ;(ref as React.MutableRefObject<T>).current = node
23
+ }
24
+ })
25
+ }
26
+ }
27
+
28
+ // MediaPlayer root component props
29
+ export interface MediaPlayerRootProps {
30
+ children: React.ReactNode
31
+ className?: string
32
+ dir?: "ltr" | "rtl"
33
+ disabled?: boolean
34
+ asChild?: boolean
35
+ autoHide?: boolean
36
+ tooltipDelayDuration?: number
37
+ tooltipSideOffset?: number
38
+ withoutTooltip?: boolean
39
+
40
+ // Mobile-first options
41
+ enableGestures?: boolean
42
+ enableMobileOptimizations?: boolean
43
+
44
+ // Media event handlers
45
+ onPlay?: () => void
46
+ onPause?: () => void
47
+ onEnded?: () => void
48
+ onTimeUpdate?: (currentTime: number) => void
49
+ onVolumeChange?: (volume: number) => void
50
+ onMuted?: (muted: boolean) => void
51
+ onMediaError?: (error: string) => void
52
+ onPipError?: (error: Error) => void
53
+ onFullscreenChange?: (isFullscreen: boolean) => void
54
+ }
55
+
56
+ // Internal MediaPlayer implementation
57
+ function MediaPlayerImpl({
58
+ children,
59
+ className,
60
+ dir = "ltr",
61
+ disabled = false,
62
+ asChild = false,
63
+ autoHide = true,
64
+ enableGestures = true,
65
+ enableMobileOptimizations = true,
66
+ onPlay,
67
+ onPause,
68
+ onEnded,
69
+ onTimeUpdate,
70
+ onVolumeChange,
71
+ onMuted,
72
+ onMediaError,
73
+ onPipError,
74
+ onFullscreenChange,
75
+ ...props
76
+ }: MediaPlayerRootProps) {
77
+ const context = useMediaContext()
78
+ const dispatch = useMediaDispatch()
79
+ const { isTouchDevice } = useDeviceInfo()
80
+
81
+ // Get state values
82
+ const isPlaying = useMediaSelector((state) => state.isPlaying)
83
+ const currentTime = useMediaSelector((state) => state.currentTime)
84
+ const volume = useMediaSelector((state) => state.volume)
85
+ const isMuted = useMediaSelector((state) => state.isMuted)
86
+ const error = useMediaSelector((state) => state.error)
87
+ const isFullscreen = useMediaSelector((state) => state.isFullscreen)
88
+ const isPictureInPicture = useMediaSelector((state) => state.isPictureInPicture)
89
+
90
+ // Enable mobile optimizations
91
+ useMobileOptimizations()
92
+
93
+ // Gesture handling actions - React 19 compiler will optimize this
94
+ const gestureActions = {
95
+ onPlayPause: () => {
96
+ if (disabled) return
97
+ dispatch({
98
+ type: isPlaying ? MediaActionTypes.PAUSE : MediaActionTypes.PLAY
99
+ })
100
+ },
101
+ onSeek: (direction: 'forward' | 'backward', amount: number) => {
102
+ if (disabled || !context.isVideo) return
103
+ const mediaElement = context.mediaRef.current
104
+ if (!mediaElement) return
105
+
106
+ const newTime = direction === 'forward'
107
+ ? Math.min(mediaElement.duration || 0, currentTime + amount)
108
+ : Math.max(0, currentTime - amount)
109
+
110
+ dispatch({
111
+ type: MediaActionTypes.SEEK,
112
+ detail: newTime
113
+ })
114
+ },
115
+ onVolumeChange: (direction: 'up' | 'down', amount: number) => {
116
+ if (disabled) return
117
+ const newVolume = direction === 'up'
118
+ ? Math.min(1, volume + amount)
119
+ : Math.max(0, volume - amount)
120
+
121
+ dispatch({
122
+ type: MediaActionTypes.SET_VOLUME,
123
+ detail: newVolume
124
+ })
125
+ },
126
+ onToggleFullscreen: () => {
127
+ if (disabled || !context.isVideo) return
128
+ dispatch({
129
+ type: isFullscreen ? MediaActionTypes.EXIT_FULLSCREEN : MediaActionTypes.ENTER_FULLSCREEN
130
+ })
131
+ },
132
+ onZoom: (scale: number) => {
133
+ // Handle video zoom - this could be implemented with CSS transforms
134
+ // For now, we'll just log it as a placeholder
135
+ console.log('Zoom gesture detected:', scale)
136
+ }
137
+ }
138
+
139
+ // Set up touch gestures for mobile devices
140
+ const { isGestureEnabled } = useMediaGestures(
141
+ context.rootRef as React.RefObject<HTMLElement>,
142
+ enableGestures ? gestureActions : {}
143
+ )
144
+
145
+ // Keyboard event handler - React 19 compiler will optimize this
146
+ const handleKeyDown = (event: React.KeyboardEvent) => {
147
+ if (disabled) return
148
+
149
+ const target = event.target as HTMLElement
150
+ const isInputElement =
151
+ target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable
152
+
153
+ if (isInputElement) return
154
+
155
+ const mediaElement = context.mediaRef.current
156
+ if (!mediaElement) return
157
+
158
+ switch (event.code) {
159
+ case "Space":
160
+ case "KeyK":
161
+ event.preventDefault()
162
+ dispatch({
163
+ type: isPlaying ? MediaActionTypes.PAUSE : MediaActionTypes.PLAY
164
+ })
165
+ break
166
+
167
+ case "KeyF":
168
+ event.preventDefault()
169
+ dispatch({
170
+ type: isFullscreen
171
+ ? MediaActionTypes.EXIT_FULLSCREEN
172
+ : MediaActionTypes.ENTER_FULLSCREEN
173
+ })
174
+ break
175
+
176
+ case "KeyM":
177
+ event.preventDefault()
178
+ dispatch({
179
+ type: MediaActionTypes.TOGGLE_MUTE
180
+ })
181
+ break
182
+
183
+ case "ArrowLeft":
184
+ event.preventDefault()
185
+ if (context.isVideo && mediaElement instanceof HTMLVideoElement) {
186
+ dispatch({
187
+ type: MediaActionTypes.SEEK,
188
+ detail: Math.max(0, currentTime - 10)
189
+ })
190
+ }
191
+ break
192
+
193
+ case "ArrowRight":
194
+ event.preventDefault()
195
+ if (context.isVideo && mediaElement instanceof HTMLVideoElement) {
196
+ dispatch({
197
+ type: MediaActionTypes.SEEK,
198
+ detail: Math.min(mediaElement.duration || 0, currentTime + 10)
199
+ })
200
+ }
201
+ break
202
+
203
+ case "ArrowUp":
204
+ event.preventDefault()
205
+ dispatch({
206
+ type: MediaActionTypes.SET_VOLUME,
207
+ detail: Math.min(1, volume + 0.1)
208
+ })
209
+ break
210
+
211
+ case "ArrowDown":
212
+ event.preventDefault()
213
+ dispatch({
214
+ type: MediaActionTypes.SET_VOLUME,
215
+ detail: Math.max(0, volume - 0.1)
216
+ })
217
+ break
218
+ }
219
+ }
220
+
221
+ // Set up media element event listeners
222
+ React.useEffect(() => {
223
+ const mediaElement = context.mediaRef.current
224
+ if (!mediaElement) return
225
+
226
+ const handlePlay = () => {
227
+ dispatch({ type: MediaActionTypes.PLAY })
228
+ onPlay?.()
229
+ }
230
+
231
+ const handlePause = () => {
232
+ dispatch({ type: MediaActionTypes.PAUSE })
233
+ onPause?.()
234
+ }
235
+
236
+ const handleEnded = () => {
237
+ dispatch({ type: MediaActionTypes.PAUSE })
238
+ onEnded?.()
239
+ }
240
+
241
+ const handleTimeUpdate = () => {
242
+ const time = mediaElement.currentTime
243
+ dispatch({ type: MediaActionTypes.SET_CURRENT_TIME, detail: time })
244
+ onTimeUpdate?.(time)
245
+ }
246
+
247
+ const handleVolumeChange = () => {
248
+ const vol = mediaElement.volume
249
+ const muted = mediaElement.muted
250
+ dispatch({ type: MediaActionTypes.SET_VOLUME, detail: vol })
251
+ dispatch({ type: muted ? MediaActionTypes.MUTE : MediaActionTypes.UNMUTE })
252
+ onVolumeChange?.(vol)
253
+ onMuted?.(muted)
254
+ }
255
+
256
+ const handleLoadedMetadata = () => {
257
+ dispatch({ type: MediaActionTypes.SET_DURATION, detail: mediaElement.duration })
258
+ dispatch({ type: MediaActionTypes.SET_LOADING, detail: false })
259
+ }
260
+
261
+ const handleError = () => {
262
+ const errorMessage = "An error occurred while loading the media"
263
+ dispatch({ type: MediaActionTypes.SET_ERROR, detail: errorMessage })
264
+ onMediaError?.(errorMessage)
265
+ }
266
+
267
+ const handleLoadStart = () => {
268
+ dispatch({ type: MediaActionTypes.SET_LOADING, detail: true })
269
+ }
270
+
271
+ // Add event listeners
272
+ mediaElement.addEventListener("play", handlePlay)
273
+ mediaElement.addEventListener("pause", handlePause)
274
+ mediaElement.addEventListener("ended", handleEnded)
275
+ mediaElement.addEventListener("timeupdate", handleTimeUpdate)
276
+ mediaElement.addEventListener("volumechange", handleVolumeChange)
277
+ mediaElement.addEventListener("loadedmetadata", handleLoadedMetadata)
278
+ mediaElement.addEventListener("error", handleError)
279
+ mediaElement.addEventListener("loadstart", handleLoadStart)
280
+
281
+ return () => {
282
+ mediaElement.removeEventListener("play", handlePlay)
283
+ mediaElement.removeEventListener("pause", handlePause)
284
+ mediaElement.removeEventListener("ended", handleEnded)
285
+ mediaElement.removeEventListener("timeupdate", handleTimeUpdate)
286
+ mediaElement.removeEventListener("volumechange", handleVolumeChange)
287
+ mediaElement.removeEventListener("loadedmetadata", handleLoadedMetadata)
288
+ mediaElement.removeEventListener("error", handleError)
289
+ mediaElement.removeEventListener("loadstart", handleLoadStart)
290
+ }
291
+ }, [dispatch, onPlay, onPause, onEnded, onTimeUpdate, onVolumeChange, onMuted, onMediaError])
292
+
293
+ const rootProps = {
294
+ "aria-labelledby": context.labelId,
295
+ "aria-describedby": context.descriptionId,
296
+ "aria-disabled": disabled,
297
+ "data-disabled": disabled ? "" : undefined,
298
+ "data-slot": "media-player",
299
+ "data-state": isFullscreen ? "fullscreen" : "inline",
300
+ dir,
301
+ tabIndex: disabled ? -1 : 0,
302
+ className: cn(
303
+ "dark relative isolate flex flex-col overflow-hidden rounded-lg bg-background outline-none",
304
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
305
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
306
+ "[&_video]:relative [&_video]:object-contain",
307
+ "data-[state=fullscreen]:[&_video]:size-full",
308
+ "[:fullscreen_&]:flex [:fullscreen_&]:h-full [:fullscreen_&]:max-h-screen [:fullscreen_&]:flex-col [:fullscreen_&]:justify-between",
309
+ // Mobile-first optimizations
310
+ isTouchDevice && [
311
+ "touch-manipulation", // Improves touch responsiveness
312
+ "select-none", // Prevents text selection during gestures
313
+ "overscroll-contain", // Prevents scroll chaining
314
+ ],
315
+ // Enable hardware acceleration for smooth animations
316
+ "will-change-transform",
317
+ "transform-gpu",
318
+ className
319
+ ),
320
+ onKeyDown: handleKeyDown,
321
+ ...props
322
+ }
323
+
324
+ return (
325
+ <div ref={composeRefs(context.rootRef)} {...rootProps}>
326
+ {children}
327
+ </div>
328
+ )
329
+ }
330
+
331
+ // Main MediaPlayer component with provider
332
+ export function MediaPlayerRoot(props: MediaPlayerRootProps) {
333
+ return (
334
+ <MediaProvider>
335
+ <MediaPlayerImpl {...props} />
336
+ </MediaProvider>
337
+ )
338
+ }
339
+
340
+ MediaPlayerRoot.displayName = "MediaPlayerRoot"
@@ -0,0 +1,168 @@
1
+ "use client"
2
+
3
+ import { Slider } from "@rp/ui/components/slider"
4
+ import React from "react"
5
+ import {
6
+ MediaActionTypes,
7
+ useMediaContext,
8
+ useMediaDispatch,
9
+ useMediaSelector
10
+ } from "../context/media-context"
11
+ import { formatTime } from "../lib/format-time"
12
+ import { cn } from "../lib/utils"
13
+
14
+ export interface MediaPlayerSeekProps {
15
+ className?: string
16
+ disabled?: boolean
17
+ withTime?: boolean
18
+ withoutTooltip?: boolean
19
+ tooltipTimeVariant?: "current" | "remaining" | "duration"
20
+ asChild?: boolean
21
+ children?: React.ReactNode
22
+ }
23
+
24
+ export function MediaPlayerSeek({
25
+ className,
26
+ disabled = false,
27
+ withTime = true,
28
+ withoutTooltip = false,
29
+ tooltipTimeVariant = "current",
30
+ asChild = false,
31
+ children,
32
+ ...props
33
+ }: MediaPlayerSeekProps) {
34
+ const context = useMediaContext()
35
+ const dispatch = useMediaDispatch()
36
+
37
+ const currentTime = useMediaSelector((state) => state.currentTime)
38
+ const duration = useMediaSelector((state) => state.duration)
39
+ const buffered = useMediaSelector((state) => state.buffered)
40
+ const isDisabled = disabled || useMediaSelector((state) => state.error !== null)
41
+
42
+ const [isDragging, setIsDragging] = React.useState(false)
43
+ const [previewTime, setPreviewTime] = React.useState<number | null>(null)
44
+
45
+ const seekableStart = 0
46
+ const seekableEnd = duration || 0
47
+ const progress = seekableEnd > 0 ? (currentTime / seekableEnd) * 100 : 0
48
+
49
+ // Calculate buffered ranges
50
+ const bufferedRanges = React.useMemo(() => {
51
+ if (!buffered || !seekableEnd) return []
52
+
53
+ const ranges: Array<{ start: number; end: number }> = []
54
+ for (let i = 0; i < buffered.length; i++) {
55
+ const start = (buffered.start(i) / seekableEnd) * 100
56
+ const end = (buffered.end(i) / seekableEnd) * 100
57
+ ranges.push({ start, end })
58
+ }
59
+ return ranges
60
+ }, [buffered, seekableEnd])
61
+
62
+ const handleValueChange = React.useCallback(
63
+ (values: number[]) => {
64
+ const time = values[0] || 0
65
+ setPreviewTime(time)
66
+
67
+ if (isDragging) {
68
+ dispatch({
69
+ type: MediaActionTypes.SEEK,
70
+ detail: time
71
+ })
72
+ }
73
+ },
74
+ [dispatch, isDragging]
75
+ )
76
+
77
+ const handleValueCommit = React.useCallback(
78
+ (values: number[]) => {
79
+ const time = values[0]
80
+ setIsDragging(false)
81
+ setPreviewTime(null)
82
+
83
+ dispatch({
84
+ type: MediaActionTypes.SEEK,
85
+ detail: time
86
+ })
87
+ },
88
+ [dispatch]
89
+ )
90
+
91
+ const handlePointerDown = React.useCallback(() => {
92
+ setIsDragging(true)
93
+ }, [])
94
+
95
+ const getTooltipTime = React.useCallback(
96
+ (time: number) => {
97
+ switch (tooltipTimeVariant) {
98
+ case "remaining":
99
+ return formatTime(Math.max(0, seekableEnd - time))
100
+ case "duration":
101
+ return formatTime(seekableEnd)
102
+ default:
103
+ return formatTime(time)
104
+ }
105
+ },
106
+ [tooltipTimeVariant, seekableEnd]
107
+ )
108
+
109
+ const sliderProps = {
110
+ "aria-controls": context.mediaId,
111
+ "aria-valuetext": `${formatTime(currentTime)} of ${formatTime(duration)}`,
112
+ "data-slot": "media-player-seek",
113
+ disabled: isDisabled,
114
+ min: seekableStart,
115
+ max: seekableEnd,
116
+ step: 0.1,
117
+ value: [previewTime ?? currentTime],
118
+ onValueChange: handleValueChange,
119
+ onValueCommit: handleValueCommit,
120
+ onPointerDown: handlePointerDown,
121
+ className: cn(
122
+ "relative flex w-full touch-none select-none items-center",
123
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
124
+ className
125
+ ),
126
+ ...props
127
+ }
128
+
129
+ const sliderContent = (
130
+ <>
131
+ {/* Track */}
132
+ <div className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
133
+ {/* Buffered ranges */}
134
+ {bufferedRanges.map((range, index) => (
135
+ <div
136
+ key={index}
137
+ className="absolute h-full bg-secondary/60"
138
+ style={{
139
+ left: `${range.start}%`,
140
+ width: `${range.end - range.start}%`
141
+ }}
142
+ />
143
+ ))}
144
+
145
+ {/* Progress */}
146
+ <div className="h-full bg-primary transition-all" style={{ width: `${progress}%` }} />
147
+ </div>
148
+
149
+ {/* Thumb */}
150
+ <div
151
+ className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
152
+ style={{
153
+ transform: `translateX(-50%)`,
154
+ left: `${progress}%`
155
+ }}
156
+ />
157
+ </>
158
+ )
159
+
160
+ if (asChild) {
161
+ const child = React.Children.only(children) as React.ReactElement
162
+ return React.cloneElement(child, sliderProps as any, sliderContent)
163
+ }
164
+
165
+ return <Slider {...sliderProps}>{sliderContent}</Slider>
166
+ }
167
+
168
+ MediaPlayerSeek.displayName = "MediaPlayerSeek"
@@ -0,0 +1,99 @@
1
+ "use client"
2
+
3
+ import React from "react"
4
+ import { useMediaContext, useMediaSelector } from "../context/media-context"
5
+ import { formatTime } from "../lib/format-time"
6
+ import { cn } from "../lib/utils"
7
+
8
+ export interface MediaPlayerTimeProps {
9
+ variant?: "current" | "duration" | "remaining" | "current-duration"
10
+ className?: string
11
+ asChild?: boolean
12
+ children?: React.ReactNode
13
+ }
14
+
15
+ export function MediaPlayerTime({
16
+ variant = "current-duration",
17
+ className,
18
+ asChild = false,
19
+ children,
20
+ ...props
21
+ }: MediaPlayerTimeProps) {
22
+ const context = useMediaContext()
23
+ const currentTime = useMediaSelector((state) => state.currentTime)
24
+ const duration = useMediaSelector((state) => state.duration)
25
+
26
+ const times = React.useMemo(() => {
27
+ if (variant === "remaining") {
28
+ return {
29
+ remaining: formatTime(Math.max(0, duration - currentTime))
30
+ }
31
+ }
32
+
33
+ if (variant === "duration") {
34
+ return {
35
+ duration: formatTime(duration)
36
+ }
37
+ }
38
+
39
+ if (variant === "current") {
40
+ return {
41
+ current: formatTime(currentTime)
42
+ }
43
+ }
44
+
45
+ return {
46
+ current: formatTime(currentTime),
47
+ duration: formatTime(duration)
48
+ }
49
+ }, [variant, currentTime, duration])
50
+
51
+ const timeProps = {
52
+ "data-slot": "media-player-time",
53
+ "data-variant": variant,
54
+ dir: context.dir,
55
+ className: cn("text-sm text-foreground/80 tabular-nums", className),
56
+ ...props
57
+ }
58
+
59
+ if (variant !== "current-duration") {
60
+ const content = times[variant as keyof typeof times]
61
+
62
+ if (asChild) {
63
+ const child = React.Children.only(children) as React.ReactElement
64
+ return React.cloneElement(child, timeProps as any, content)
65
+ }
66
+
67
+ return <time {...timeProps}>{content}</time>
68
+ }
69
+
70
+ const content = (
71
+ <>
72
+ <time>{times.current}</time>
73
+ <span className="text-foreground/60">/</span>
74
+ <time>{times.duration}</time>
75
+ </>
76
+ )
77
+
78
+ if (asChild) {
79
+ const child = React.Children.only(children) as React.ReactElement
80
+ return React.cloneElement(
81
+ child,
82
+ {
83
+ ...timeProps,
84
+ className: cn("flex items-center gap-1 text-foreground/80 text-sm", className)
85
+ } as any,
86
+ content
87
+ )
88
+ }
89
+
90
+ return (
91
+ <div
92
+ {...timeProps}
93
+ className={cn("flex items-center gap-1 text-foreground/80 text-sm", className)}>
94
+ {content}
95
+ </div>
96
+ )
97
+ }
98
+
99
+ MediaPlayerTime.displayName = "MediaPlayerTime"