@videojs/react 10.0.0-beta.24 → 10.0.0-beta.25

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 (713) hide show
  1. package/dist/default/icons/dist/react/default/airplay-enter.js +16 -0
  2. package/dist/default/icons/dist/react/default/airplay-enter.js.map +1 -0
  3. package/dist/default/icons/dist/react/default/airplay-exit.js +28 -0
  4. package/dist/default/icons/dist/react/default/airplay-exit.js.map +1 -0
  5. package/dist/default/icons/dist/react/default/captions-off.js +5 -0
  6. package/dist/default/icons/dist/react/default/captions-off.js.map +1 -1
  7. package/dist/default/icons/dist/react/default/gear.js +16 -0
  8. package/dist/default/icons/dist/react/default/gear.js.map +1 -0
  9. package/dist/default/icons/dist/react/default/quality.js +16 -0
  10. package/dist/default/icons/dist/react/default/quality.js.map +1 -0
  11. package/dist/default/icons/dist/react/default/speech.js +19 -0
  12. package/dist/default/icons/dist/react/default/speech.js.map +1 -0
  13. package/dist/default/icons/dist/react/default/speed.js +52 -0
  14. package/dist/default/icons/dist/react/default/speed.js.map +1 -0
  15. package/dist/default/icons/dist/react/default/switches.js +19 -0
  16. package/dist/default/icons/dist/react/default/switches.js.map +1 -0
  17. package/dist/default/icons/dist/react/minimal/airplay-enter.js +16 -0
  18. package/dist/default/icons/dist/react/minimal/airplay-enter.js.map +1 -0
  19. package/dist/default/icons/dist/react/minimal/airplay-exit.js +28 -0
  20. package/dist/default/icons/dist/react/minimal/airplay-exit.js.map +1 -0
  21. package/dist/default/icons/dist/react/minimal/captions-off.js +5 -0
  22. package/dist/default/icons/dist/react/minimal/captions-off.js.map +1 -1
  23. package/dist/default/icons/dist/react/minimal/gear.js +16 -0
  24. package/dist/default/icons/dist/react/minimal/gear.js.map +1 -0
  25. package/dist/default/icons/dist/react/minimal/quality.js +16 -0
  26. package/dist/default/icons/dist/react/minimal/quality.js.map +1 -0
  27. package/dist/default/icons/dist/react/minimal/speech.js +54 -0
  28. package/dist/default/icons/dist/react/minimal/speech.js.map +1 -0
  29. package/dist/default/icons/dist/react/minimal/speed.js +52 -0
  30. package/dist/default/icons/dist/react/minimal/speed.js.map +1 -0
  31. package/dist/default/icons/dist/react/minimal/switches.js +19 -0
  32. package/dist/default/icons/dist/react/minimal/switches.js.map +1 -0
  33. package/dist/default/icons/index.js +8 -1
  34. package/dist/default/icons/minimal/index.js +8 -1
  35. package/dist/default/index.js +21 -17
  36. package/dist/default/media/dash-video/index.js +5 -1
  37. package/dist/default/media/dash-video/index.js.map +1 -1
  38. package/dist/default/media/{hls-video → hlsjs-video}/index.js +9 -5
  39. package/dist/default/media/hlsjs-video/index.js.map +1 -0
  40. package/dist/default/media/mux-audio/index.js +9 -3
  41. package/dist/default/media/mux-audio/index.js.map +1 -1
  42. package/dist/default/media/mux-video/index.js +9 -3
  43. package/dist/default/media/mux-video/index.js.map +1 -1
  44. package/dist/default/media/native-hls-video/index.js +5 -1
  45. package/dist/default/media/native-hls-video/index.js.map +1 -1
  46. package/dist/default/media/simple-hls-audio-only/index.js +22 -0
  47. package/dist/default/media/simple-hls-audio-only/index.js.map +1 -0
  48. package/dist/default/media/simple-hls-video/index.js +1 -1
  49. package/dist/default/media/vimeo-video/index.js +37 -0
  50. package/dist/default/media/vimeo-video/index.js.map +1 -0
  51. package/dist/default/player/context.js.map +1 -1
  52. package/dist/default/player/create-player.js +13 -4
  53. package/dist/default/player/create-player.js.map +1 -1
  54. package/dist/default/presets/audio/minimal-skin.css +414 -69
  55. package/dist/default/presets/audio/minimal-skin.js +69 -39
  56. package/dist/default/presets/audio/minimal-skin.js.map +1 -1
  57. package/dist/default/presets/audio/minimal-skin.tailwind.js +71 -42
  58. package/dist/default/presets/audio/minimal-skin.tailwind.js.map +1 -1
  59. package/dist/default/presets/audio/skin.css +419 -121
  60. package/dist/default/presets/audio/skin.js +70 -40
  61. package/dist/default/presets/audio/skin.js.map +1 -1
  62. package/dist/default/presets/audio/skin.tailwind.js +72 -42
  63. package/dist/default/presets/audio/skin.tailwind.js.map +1 -1
  64. package/dist/default/presets/live-audio/index.js +2 -2
  65. package/dist/default/presets/live-audio/minimal-skin.css +414 -69
  66. package/dist/default/presets/live-audio/minimal-skin.js +26 -12
  67. package/dist/default/presets/live-audio/minimal-skin.js.map +1 -1
  68. package/dist/default/presets/live-audio/minimal-skin.tailwind.js +27 -14
  69. package/dist/default/presets/live-audio/minimal-skin.tailwind.js.map +1 -1
  70. package/dist/default/presets/live-audio/skin.css +419 -121
  71. package/dist/default/presets/live-audio/skin.js +32 -14
  72. package/dist/default/presets/live-audio/skin.js.map +1 -1
  73. package/dist/default/presets/live-audio/skin.tailwind.js +27 -13
  74. package/dist/default/presets/live-audio/skin.tailwind.js.map +1 -1
  75. package/dist/default/presets/live-video/minimal-skin.css +566 -210
  76. package/dist/default/presets/live-video/minimal-skin.js +93 -12
  77. package/dist/default/presets/live-video/minimal-skin.js.map +1 -1
  78. package/dist/default/presets/live-video/minimal-skin.tailwind.js +94 -13
  79. package/dist/default/presets/live-video/minimal-skin.tailwind.js.map +1 -1
  80. package/dist/default/presets/live-video/skin.css +581 -264
  81. package/dist/default/presets/live-video/skin.js +95 -16
  82. package/dist/default/presets/live-video/skin.js.map +1 -1
  83. package/dist/default/presets/live-video/skin.tailwind.js +95 -17
  84. package/dist/default/presets/live-video/skin.tailwind.js.map +1 -1
  85. package/dist/default/presets/video/minimal-skin.css +566 -210
  86. package/dist/default/presets/video/minimal-skin.js +281 -53
  87. package/dist/default/presets/video/minimal-skin.js.map +1 -1
  88. package/dist/default/presets/video/minimal-skin.tailwind.js +284 -57
  89. package/dist/default/presets/video/minimal-skin.tailwind.js.map +1 -1
  90. package/dist/default/presets/video/skin.css +581 -264
  91. package/dist/default/presets/video/skin.js +282 -57
  92. package/dist/default/presets/video/skin.js.map +1 -1
  93. package/dist/default/presets/video/skin.tailwind.js +285 -61
  94. package/dist/default/presets/video/skin.tailwind.js.map +1 -1
  95. package/dist/default/skins/dist/default/default/tailwind/audio.tailwind.js +8 -3
  96. package/dist/default/skins/dist/default/default/tailwind/audio.tailwind.js.map +1 -1
  97. package/dist/default/skins/dist/default/default/tailwind/components/badge.js +6 -0
  98. package/dist/default/skins/dist/default/default/tailwind/components/badge.js.map +1 -0
  99. package/dist/default/skins/dist/default/default/tailwind/components/buffering.js +1 -4
  100. package/dist/default/skins/dist/default/default/tailwind/components/buffering.js.map +1 -1
  101. package/dist/default/skins/dist/default/default/tailwind/components/icon.js +1 -1
  102. package/dist/default/skins/dist/default/default/tailwind/components/icon.js.map +1 -1
  103. package/dist/default/skins/dist/default/default/tailwind/components/input-feedback.js +9 -9
  104. package/dist/default/skins/dist/default/default/tailwind/components/input-feedback.js.map +1 -1
  105. package/dist/default/skins/dist/default/default/tailwind/components/menu.js +26 -4
  106. package/dist/default/skins/dist/default/default/tailwind/components/menu.js.map +1 -1
  107. package/dist/default/skins/dist/default/default/tailwind/components/overlay.js +1 -1
  108. package/dist/default/skins/dist/default/default/tailwind/components/overlay.js.map +1 -1
  109. package/dist/default/skins/dist/default/default/tailwind/components/popup.js +4 -3
  110. package/dist/default/skins/dist/default/default/tailwind/components/popup.js.map +1 -1
  111. package/dist/default/skins/dist/default/default/tailwind/components/poster.js +7 -1
  112. package/dist/default/skins/dist/default/default/tailwind/components/poster.js.map +1 -1
  113. package/dist/default/skins/dist/default/default/tailwind/components/root.js +1 -1
  114. package/dist/default/skins/dist/default/default/tailwind/components/root.js.map +1 -1
  115. package/dist/default/skins/dist/default/default/tailwind/components/seek.js +1 -1
  116. package/dist/default/skins/dist/default/default/tailwind/components/seek.js.map +1 -1
  117. package/dist/default/skins/dist/default/default/tailwind/components/slider.js +5 -3
  118. package/dist/default/skins/dist/default/default/tailwind/components/slider.js.map +1 -1
  119. package/dist/default/skins/dist/default/default/tailwind/components/thumbnail.js +12 -0
  120. package/dist/default/skins/dist/default/default/tailwind/components/thumbnail.js.map +1 -0
  121. package/dist/default/skins/dist/default/default/tailwind/components/time.js +1 -1
  122. package/dist/default/skins/dist/default/default/tailwind/components/time.js.map +1 -1
  123. package/dist/default/skins/dist/default/default/tailwind/video.tailwind.js +26 -13
  124. package/dist/default/skins/dist/default/default/tailwind/video.tailwind.js.map +1 -1
  125. package/dist/default/skins/dist/default/minimal/tailwind/audio.tailwind.js +13 -3
  126. package/dist/default/skins/dist/default/minimal/tailwind/audio.tailwind.js.map +1 -1
  127. package/dist/default/skins/dist/default/minimal/tailwind/components/badge.js +6 -0
  128. package/dist/default/skins/dist/default/minimal/tailwind/components/badge.js.map +1 -0
  129. package/dist/default/skins/dist/default/minimal/tailwind/components/buffering.js +1 -1
  130. package/dist/default/skins/dist/default/minimal/tailwind/components/buffering.js.map +1 -1
  131. package/dist/default/skins/dist/default/minimal/tailwind/components/icon.js +1 -1
  132. package/dist/default/skins/dist/default/minimal/tailwind/components/icon.js.map +1 -1
  133. package/dist/default/skins/dist/default/minimal/tailwind/components/input-feedback.js +9 -9
  134. package/dist/default/skins/dist/default/minimal/tailwind/components/input-feedback.js.map +1 -1
  135. package/dist/default/skins/dist/default/minimal/tailwind/components/menu.js +25 -4
  136. package/dist/default/skins/dist/default/minimal/tailwind/components/menu.js.map +1 -1
  137. package/dist/default/skins/dist/default/minimal/tailwind/components/overlay.js +1 -1
  138. package/dist/default/skins/dist/default/minimal/tailwind/components/overlay.js.map +1 -1
  139. package/dist/default/skins/dist/default/minimal/tailwind/components/popup.js +3 -2
  140. package/dist/default/skins/dist/default/minimal/tailwind/components/popup.js.map +1 -1
  141. package/dist/default/skins/dist/default/minimal/tailwind/components/poster.js +7 -1
  142. package/dist/default/skins/dist/default/minimal/tailwind/components/poster.js.map +1 -1
  143. package/dist/default/skins/dist/default/minimal/tailwind/components/root.js +1 -1
  144. package/dist/default/skins/dist/default/minimal/tailwind/components/root.js.map +1 -1
  145. package/dist/default/skins/dist/default/minimal/tailwind/components/seek.js +1 -1
  146. package/dist/default/skins/dist/default/minimal/tailwind/components/seek.js.map +1 -1
  147. package/dist/default/skins/dist/default/minimal/tailwind/components/slider.js +5 -3
  148. package/dist/default/skins/dist/default/minimal/tailwind/components/slider.js.map +1 -1
  149. package/dist/default/skins/dist/default/minimal/tailwind/components/thumbnail.js +13 -0
  150. package/dist/default/skins/dist/default/minimal/tailwind/components/thumbnail.js.map +1 -0
  151. package/dist/default/skins/dist/default/minimal/tailwind/components/time.js +1 -1
  152. package/dist/default/skins/dist/default/minimal/tailwind/components/time.js.map +1 -1
  153. package/dist/default/skins/dist/default/minimal/tailwind/video.tailwind.js +27 -11
  154. package/dist/default/skins/dist/default/minimal/tailwind/video.tailwind.js.map +1 -1
  155. package/dist/default/skins/dist/default/shared/tailwind/icon-state.js +17 -2
  156. package/dist/default/skins/dist/default/shared/tailwind/icon-state.js.map +1 -1
  157. package/dist/default/tailwind.css +0 -24
  158. package/dist/default/ui/airplay-button/airplay-button.js +17 -0
  159. package/dist/default/ui/airplay-button/airplay-button.js.map +1 -0
  160. package/dist/default/ui/airplay-button/index.js +2 -0
  161. package/dist/default/ui/alert-dialog/alert-dialog-close.js +1 -1
  162. package/dist/default/ui/alert-dialog/alert-dialog-popup.js +1 -1
  163. package/dist/default/ui/audio-track/index.js +2 -0
  164. package/dist/default/ui/audio-track/use-audio-track-options.js +31 -0
  165. package/dist/default/ui/audio-track/use-audio-track-options.js.map +1 -0
  166. package/dist/default/ui/captions-radio-group/index.js +2 -0
  167. package/dist/default/ui/captions-radio-group/use-captions-options.js +37 -0
  168. package/dist/default/ui/captions-radio-group/use-captions-options.js.map +1 -0
  169. package/dist/default/ui/create-media-button.js +23 -10
  170. package/dist/default/ui/create-media-button.js.map +1 -1
  171. package/dist/default/ui/error-dialog/error-dialog-description.js +1 -1
  172. package/dist/default/ui/hooks/use-button.js.map +1 -1
  173. package/dist/default/ui/hotkey/use-hotkey-shortcut.js +28 -0
  174. package/dist/default/ui/hotkey/use-hotkey-shortcut.js.map +1 -0
  175. package/dist/default/ui/hotkey/use-hotkey.js +1 -2
  176. package/dist/default/ui/hotkey/use-hotkey.js.map +1 -1
  177. package/dist/default/ui/input-indicators/use-input-indicator-root.js +2 -2
  178. package/dist/default/ui/input-indicators/use-input-indicator-root.js.map +1 -1
  179. package/dist/default/ui/input-indicators/use-rendered-indicator-state.js +7 -4
  180. package/dist/default/ui/input-indicators/use-rendered-indicator-state.js.map +1 -1
  181. package/dist/default/ui/live-button/live-button.js +1 -1
  182. package/dist/default/ui/live-button/live-button.js.map +1 -1
  183. package/dist/default/ui/menu/context.js +16 -1
  184. package/dist/default/ui/menu/context.js.map +1 -1
  185. package/dist/default/ui/menu/index.parts.js +4 -2
  186. package/dist/default/ui/menu/index.parts.js.map +1 -1
  187. package/dist/default/ui/menu/menu-back.js +0 -1
  188. package/dist/default/ui/menu/menu-back.js.map +1 -1
  189. package/dist/default/ui/menu/menu-checkbox-item.js +1 -2
  190. package/dist/default/ui/menu/menu-checkbox-item.js.map +1 -1
  191. package/dist/default/ui/menu/menu-content.js +23 -5
  192. package/dist/default/ui/menu/menu-content.js.map +1 -1
  193. package/dist/default/ui/menu/menu-group-label.js +29 -0
  194. package/dist/default/ui/menu/menu-group-label.js.map +1 -0
  195. package/dist/default/ui/menu/menu-group.js +7 -9
  196. package/dist/default/ui/menu/menu-group.js.map +1 -1
  197. package/dist/default/ui/menu/menu-item-indicator.js +1 -2
  198. package/dist/default/ui/menu/menu-item-indicator.js.map +1 -1
  199. package/dist/default/ui/menu/menu-item-setting-provider.js +72 -0
  200. package/dist/default/ui/menu/menu-item-setting-provider.js.map +1 -0
  201. package/dist/default/ui/menu/menu-item-value.js +28 -0
  202. package/dist/default/ui/menu/menu-item-value.js.map +1 -0
  203. package/dist/default/ui/menu/menu-item.js +44 -23
  204. package/dist/default/ui/menu/menu-item.js.map +1 -1
  205. package/dist/default/ui/menu/menu-radio-group.js +6 -9
  206. package/dist/default/ui/menu/menu-radio-group.js.map +1 -1
  207. package/dist/default/ui/menu/menu-radio-item.js +1 -2
  208. package/dist/default/ui/menu/menu-radio-item.js.map +1 -1
  209. package/dist/default/ui/menu/menu-root.js +1 -1
  210. package/dist/default/ui/menu/menu-separator.js +1 -2
  211. package/dist/default/ui/menu/menu-separator.js.map +1 -1
  212. package/dist/default/ui/menu/menu-trigger.js +85 -29
  213. package/dist/default/ui/menu/menu-trigger.js.map +1 -1
  214. package/dist/default/ui/menu/use-menu-group.js +31 -0
  215. package/dist/default/ui/menu/use-menu-group.js.map +1 -0
  216. package/dist/default/ui/playback-rate/index.js +2 -0
  217. package/dist/default/ui/playback-rate/use-playback-rate-options.js +34 -0
  218. package/dist/default/ui/playback-rate/use-playback-rate-options.js.map +1 -0
  219. package/dist/default/ui/playback-rate-button/playback-rate-button.js +2 -1
  220. package/dist/default/ui/playback-rate-button/playback-rate-button.js.map +1 -1
  221. package/dist/default/ui/poster/poster.js +17 -3
  222. package/dist/default/ui/poster/poster.js.map +1 -1
  223. package/dist/default/ui/quality/index.js +2 -0
  224. package/dist/default/ui/quality/use-quality-options.js +37 -0
  225. package/dist/default/ui/quality/use-quality-options.js.map +1 -0
  226. package/dist/default/ui/seek-button/seek-button.js +3 -1
  227. package/dist/default/ui/seek-button/seek-button.js.map +1 -1
  228. package/dist/default/ui/time/time-value.js +32 -5
  229. package/dist/default/ui/time/time-value.js.map +1 -1
  230. package/dist/default/ui/time-slider/time-slider-root.js +18 -6
  231. package/dist/default/ui/time-slider/time-slider-root.js.map +1 -1
  232. package/dist/default/ui/tooltip/context.js.map +1 -1
  233. package/dist/default/ui/tooltip/index.parts.js +4 -0
  234. package/dist/default/ui/tooltip/index.parts.js.map +1 -1
  235. package/dist/default/ui/tooltip/tooltip-arrow.js +1 -1
  236. package/dist/default/ui/tooltip/tooltip-label.js +24 -0
  237. package/dist/default/ui/tooltip/tooltip-label.js.map +1 -0
  238. package/dist/default/ui/tooltip/tooltip-popup.js +7 -3
  239. package/dist/default/ui/tooltip/tooltip-popup.js.map +1 -1
  240. package/dist/default/ui/tooltip/tooltip-root.js +1 -1
  241. package/dist/default/ui/tooltip/tooltip-root.js.map +1 -1
  242. package/dist/default/ui/tooltip/tooltip-shortcut.js +25 -0
  243. package/dist/default/ui/tooltip/tooltip-shortcut.js.map +1 -0
  244. package/dist/default/ui/volume-indicator/volume-indicator-root.js +1 -1
  245. package/dist/default/ui/volume-indicator/volume-indicator-root.js.map +1 -1
  246. package/dist/default/utils/use-attach-iframe.js +14 -0
  247. package/dist/default/utils/use-attach-iframe.js.map +1 -0
  248. package/dist/default/utils/use-media-instance.js +11 -3
  249. package/dist/default/utils/use-media-instance.js.map +1 -1
  250. package/dist/default/utils/use-render.js +1 -1
  251. package/dist/default/utils/use-render.js.map +1 -1
  252. package/dist/dev/icons/dist/react/default/airplay-enter.js +16 -0
  253. package/dist/dev/icons/dist/react/default/airplay-enter.js.map +1 -0
  254. package/dist/dev/icons/dist/react/default/airplay-exit.js +28 -0
  255. package/dist/dev/icons/dist/react/default/airplay-exit.js.map +1 -0
  256. package/dist/dev/icons/dist/react/default/captions-off.js +5 -0
  257. package/dist/dev/icons/dist/react/default/captions-off.js.map +1 -1
  258. package/dist/dev/icons/dist/react/default/gear.js +16 -0
  259. package/dist/dev/icons/dist/react/default/gear.js.map +1 -0
  260. package/dist/dev/icons/dist/react/default/index.d.ts +8 -1
  261. package/dist/dev/icons/dist/react/default/index.d.ts.map +1 -1
  262. package/dist/dev/icons/dist/react/default/quality.js +16 -0
  263. package/dist/dev/icons/dist/react/default/quality.js.map +1 -0
  264. package/dist/dev/icons/dist/react/default/speech.js +19 -0
  265. package/dist/dev/icons/dist/react/default/speech.js.map +1 -0
  266. package/dist/dev/icons/dist/react/default/speed.js +52 -0
  267. package/dist/dev/icons/dist/react/default/speed.js.map +1 -0
  268. package/dist/dev/icons/dist/react/default/switches.js +19 -0
  269. package/dist/dev/icons/dist/react/default/switches.js.map +1 -0
  270. package/dist/dev/icons/dist/react/minimal/airplay-enter.js +16 -0
  271. package/dist/dev/icons/dist/react/minimal/airplay-enter.js.map +1 -0
  272. package/dist/dev/icons/dist/react/minimal/airplay-exit.js +28 -0
  273. package/dist/dev/icons/dist/react/minimal/airplay-exit.js.map +1 -0
  274. package/dist/dev/icons/dist/react/minimal/captions-off.js +5 -0
  275. package/dist/dev/icons/dist/react/minimal/captions-off.js.map +1 -1
  276. package/dist/dev/icons/dist/react/minimal/gear.js +16 -0
  277. package/dist/dev/icons/dist/react/minimal/gear.js.map +1 -0
  278. package/dist/dev/icons/dist/react/minimal/index.d.ts +8 -1
  279. package/dist/dev/icons/dist/react/minimal/index.d.ts.map +1 -1
  280. package/dist/dev/icons/dist/react/minimal/quality.js +16 -0
  281. package/dist/dev/icons/dist/react/minimal/quality.js.map +1 -0
  282. package/dist/dev/icons/dist/react/minimal/speech.js +54 -0
  283. package/dist/dev/icons/dist/react/minimal/speech.js.map +1 -0
  284. package/dist/dev/icons/dist/react/minimal/speed.js +52 -0
  285. package/dist/dev/icons/dist/react/minimal/speed.js.map +1 -0
  286. package/dist/dev/icons/dist/react/minimal/switches.js +19 -0
  287. package/dist/dev/icons/dist/react/minimal/switches.js.map +1 -0
  288. package/dist/dev/icons/index.d.ts +2 -2
  289. package/dist/dev/icons/index.js +8 -1
  290. package/dist/dev/icons/minimal/index.d.ts +2 -2
  291. package/dist/dev/icons/minimal/index.js +8 -1
  292. package/dist/dev/index.d.ts +19 -15
  293. package/dist/dev/index.js +21 -17
  294. package/dist/dev/media/dash-video/index.d.ts.map +1 -1
  295. package/dist/dev/media/dash-video/index.js +5 -1
  296. package/dist/dev/media/dash-video/index.js.map +1 -1
  297. package/dist/dev/media/hlsjs-video/index.d.ts +15 -0
  298. package/dist/dev/media/hlsjs-video/index.d.ts.map +1 -0
  299. package/dist/dev/media/{hls-video → hlsjs-video}/index.js +9 -5
  300. package/dist/dev/media/hlsjs-video/index.js.map +1 -0
  301. package/dist/dev/media/mux-audio/index.d.ts +2 -2
  302. package/dist/dev/media/mux-audio/index.d.ts.map +1 -1
  303. package/dist/dev/media/mux-audio/index.js +9 -3
  304. package/dist/dev/media/mux-audio/index.js.map +1 -1
  305. package/dist/dev/media/mux-video/index.d.ts +2 -2
  306. package/dist/dev/media/mux-video/index.d.ts.map +1 -1
  307. package/dist/dev/media/mux-video/index.js +9 -3
  308. package/dist/dev/media/mux-video/index.js.map +1 -1
  309. package/dist/dev/media/native-hls-video/index.d.ts.map +1 -1
  310. package/dist/dev/media/native-hls-video/index.js +5 -1
  311. package/dist/dev/media/native-hls-video/index.js.map +1 -1
  312. package/dist/dev/media/simple-hls-audio-only/index.d.ts +15 -0
  313. package/dist/dev/media/simple-hls-audio-only/index.d.ts.map +1 -0
  314. package/dist/dev/media/simple-hls-audio-only/index.js +22 -0
  315. package/dist/dev/media/simple-hls-audio-only/index.js.map +1 -0
  316. package/dist/dev/media/simple-hls-video/index.js +1 -1
  317. package/dist/dev/media/vimeo-video/index.d.ts +15 -0
  318. package/dist/dev/media/vimeo-video/index.d.ts.map +1 -0
  319. package/dist/dev/media/vimeo-video/index.js +37 -0
  320. package/dist/dev/media/vimeo-video/index.js.map +1 -0
  321. package/dist/dev/player/context.d.ts +8 -0
  322. package/dist/dev/player/context.d.ts.map +1 -1
  323. package/dist/dev/player/context.js.map +1 -1
  324. package/dist/dev/player/create-player.js +13 -4
  325. package/dist/dev/player/create-player.js.map +1 -1
  326. package/dist/dev/presets/audio/minimal-skin.css +414 -69
  327. package/dist/dev/presets/audio/minimal-skin.d.ts.map +1 -1
  328. package/dist/dev/presets/audio/minimal-skin.js +69 -39
  329. package/dist/dev/presets/audio/minimal-skin.js.map +1 -1
  330. package/dist/dev/presets/audio/minimal-skin.tailwind.d.ts.map +1 -1
  331. package/dist/dev/presets/audio/minimal-skin.tailwind.js +71 -42
  332. package/dist/dev/presets/audio/minimal-skin.tailwind.js.map +1 -1
  333. package/dist/dev/presets/audio/skin.css +419 -121
  334. package/dist/dev/presets/audio/skin.d.ts.map +1 -1
  335. package/dist/dev/presets/audio/skin.js +70 -40
  336. package/dist/dev/presets/audio/skin.js.map +1 -1
  337. package/dist/dev/presets/audio/skin.tailwind.d.ts.map +1 -1
  338. package/dist/dev/presets/audio/skin.tailwind.js +72 -42
  339. package/dist/dev/presets/audio/skin.tailwind.js.map +1 -1
  340. package/dist/dev/presets/live-audio/index.d.ts +2 -2
  341. package/dist/dev/presets/live-audio/index.js +2 -2
  342. package/dist/dev/presets/live-audio/minimal-skin.css +414 -69
  343. package/dist/dev/presets/live-audio/minimal-skin.d.ts.map +1 -1
  344. package/dist/dev/presets/live-audio/minimal-skin.js +26 -12
  345. package/dist/dev/presets/live-audio/minimal-skin.js.map +1 -1
  346. package/dist/dev/presets/live-audio/minimal-skin.tailwind.d.ts.map +1 -1
  347. package/dist/dev/presets/live-audio/minimal-skin.tailwind.js +27 -14
  348. package/dist/dev/presets/live-audio/minimal-skin.tailwind.js.map +1 -1
  349. package/dist/dev/presets/live-audio/skin.css +419 -121
  350. package/dist/dev/presets/live-audio/skin.d.ts +3 -1
  351. package/dist/dev/presets/live-audio/skin.d.ts.map +1 -1
  352. package/dist/dev/presets/live-audio/skin.js +32 -14
  353. package/dist/dev/presets/live-audio/skin.js.map +1 -1
  354. package/dist/dev/presets/live-audio/skin.tailwind.d.ts.map +1 -1
  355. package/dist/dev/presets/live-audio/skin.tailwind.js +27 -13
  356. package/dist/dev/presets/live-audio/skin.tailwind.js.map +1 -1
  357. package/dist/dev/presets/live-video/minimal-skin.css +566 -210
  358. package/dist/dev/presets/live-video/minimal-skin.d.ts +0 -7
  359. package/dist/dev/presets/live-video/minimal-skin.d.ts.map +1 -1
  360. package/dist/dev/presets/live-video/minimal-skin.js +93 -12
  361. package/dist/dev/presets/live-video/minimal-skin.js.map +1 -1
  362. package/dist/dev/presets/live-video/minimal-skin.tailwind.d.ts.map +1 -1
  363. package/dist/dev/presets/live-video/minimal-skin.tailwind.js +94 -13
  364. package/dist/dev/presets/live-video/minimal-skin.tailwind.js.map +1 -1
  365. package/dist/dev/presets/live-video/skin.css +581 -264
  366. package/dist/dev/presets/live-video/skin.d.ts +0 -6
  367. package/dist/dev/presets/live-video/skin.d.ts.map +1 -1
  368. package/dist/dev/presets/live-video/skin.js +95 -16
  369. package/dist/dev/presets/live-video/skin.js.map +1 -1
  370. package/dist/dev/presets/live-video/skin.tailwind.d.ts.map +1 -1
  371. package/dist/dev/presets/live-video/skin.tailwind.js +95 -17
  372. package/dist/dev/presets/live-video/skin.tailwind.js.map +1 -1
  373. package/dist/dev/presets/types.d.ts +2 -1
  374. package/dist/dev/presets/types.d.ts.map +1 -1
  375. package/dist/dev/presets/video/minimal-skin.css +566 -210
  376. package/dist/dev/presets/video/minimal-skin.d.ts.map +1 -1
  377. package/dist/dev/presets/video/minimal-skin.js +281 -53
  378. package/dist/dev/presets/video/minimal-skin.js.map +1 -1
  379. package/dist/dev/presets/video/minimal-skin.tailwind.d.ts.map +1 -1
  380. package/dist/dev/presets/video/minimal-skin.tailwind.js +284 -57
  381. package/dist/dev/presets/video/minimal-skin.tailwind.js.map +1 -1
  382. package/dist/dev/presets/video/skin.css +581 -264
  383. package/dist/dev/presets/video/skin.d.ts.map +1 -1
  384. package/dist/dev/presets/video/skin.js +282 -57
  385. package/dist/dev/presets/video/skin.js.map +1 -1
  386. package/dist/dev/presets/video/skin.tailwind.d.ts.map +1 -1
  387. package/dist/dev/presets/video/skin.tailwind.js +285 -61
  388. package/dist/dev/presets/video/skin.tailwind.js.map +1 -1
  389. package/dist/dev/skins/dist/default/default/tailwind/audio.tailwind.js +8 -3
  390. package/dist/dev/skins/dist/default/default/tailwind/audio.tailwind.js.map +1 -1
  391. package/dist/dev/skins/dist/default/default/tailwind/components/badge.js +6 -0
  392. package/dist/dev/skins/dist/default/default/tailwind/components/badge.js.map +1 -0
  393. package/dist/dev/skins/dist/default/default/tailwind/components/buffering.js +1 -4
  394. package/dist/dev/skins/dist/default/default/tailwind/components/buffering.js.map +1 -1
  395. package/dist/dev/skins/dist/default/default/tailwind/components/icon.js +1 -1
  396. package/dist/dev/skins/dist/default/default/tailwind/components/icon.js.map +1 -1
  397. package/dist/dev/skins/dist/default/default/tailwind/components/input-feedback.js +9 -9
  398. package/dist/dev/skins/dist/default/default/tailwind/components/input-feedback.js.map +1 -1
  399. package/dist/dev/skins/dist/default/default/tailwind/components/menu.js +26 -4
  400. package/dist/dev/skins/dist/default/default/tailwind/components/menu.js.map +1 -1
  401. package/dist/dev/skins/dist/default/default/tailwind/components/overlay.js +1 -1
  402. package/dist/dev/skins/dist/default/default/tailwind/components/overlay.js.map +1 -1
  403. package/dist/dev/skins/dist/default/default/tailwind/components/popup.js +4 -3
  404. package/dist/dev/skins/dist/default/default/tailwind/components/popup.js.map +1 -1
  405. package/dist/dev/skins/dist/default/default/tailwind/components/poster.js +7 -1
  406. package/dist/dev/skins/dist/default/default/tailwind/components/poster.js.map +1 -1
  407. package/dist/dev/skins/dist/default/default/tailwind/components/root.js +1 -1
  408. package/dist/dev/skins/dist/default/default/tailwind/components/root.js.map +1 -1
  409. package/dist/dev/skins/dist/default/default/tailwind/components/seek.js +1 -1
  410. package/dist/dev/skins/dist/default/default/tailwind/components/seek.js.map +1 -1
  411. package/dist/dev/skins/dist/default/default/tailwind/components/slider.js +5 -3
  412. package/dist/dev/skins/dist/default/default/tailwind/components/slider.js.map +1 -1
  413. package/dist/dev/skins/dist/default/default/tailwind/components/thumbnail.js +12 -0
  414. package/dist/dev/skins/dist/default/default/tailwind/components/thumbnail.js.map +1 -0
  415. package/dist/dev/skins/dist/default/default/tailwind/components/time.js +1 -1
  416. package/dist/dev/skins/dist/default/default/tailwind/components/time.js.map +1 -1
  417. package/dist/dev/skins/dist/default/default/tailwind/video.tailwind.js +26 -13
  418. package/dist/dev/skins/dist/default/default/tailwind/video.tailwind.js.map +1 -1
  419. package/dist/dev/skins/dist/default/minimal/tailwind/audio.tailwind.js +13 -3
  420. package/dist/dev/skins/dist/default/minimal/tailwind/audio.tailwind.js.map +1 -1
  421. package/dist/dev/skins/dist/default/minimal/tailwind/components/badge.js +6 -0
  422. package/dist/dev/skins/dist/default/minimal/tailwind/components/badge.js.map +1 -0
  423. package/dist/dev/skins/dist/default/minimal/tailwind/components/buffering.js +1 -1
  424. package/dist/dev/skins/dist/default/minimal/tailwind/components/buffering.js.map +1 -1
  425. package/dist/dev/skins/dist/default/minimal/tailwind/components/icon.js +1 -1
  426. package/dist/dev/skins/dist/default/minimal/tailwind/components/icon.js.map +1 -1
  427. package/dist/dev/skins/dist/default/minimal/tailwind/components/input-feedback.js +9 -9
  428. package/dist/dev/skins/dist/default/minimal/tailwind/components/input-feedback.js.map +1 -1
  429. package/dist/dev/skins/dist/default/minimal/tailwind/components/menu.js +25 -4
  430. package/dist/dev/skins/dist/default/minimal/tailwind/components/menu.js.map +1 -1
  431. package/dist/dev/skins/dist/default/minimal/tailwind/components/overlay.js +1 -1
  432. package/dist/dev/skins/dist/default/minimal/tailwind/components/overlay.js.map +1 -1
  433. package/dist/dev/skins/dist/default/minimal/tailwind/components/popup.js +3 -2
  434. package/dist/dev/skins/dist/default/minimal/tailwind/components/popup.js.map +1 -1
  435. package/dist/dev/skins/dist/default/minimal/tailwind/components/poster.js +7 -1
  436. package/dist/dev/skins/dist/default/minimal/tailwind/components/poster.js.map +1 -1
  437. package/dist/dev/skins/dist/default/minimal/tailwind/components/root.js +1 -1
  438. package/dist/dev/skins/dist/default/minimal/tailwind/components/root.js.map +1 -1
  439. package/dist/dev/skins/dist/default/minimal/tailwind/components/seek.js +1 -1
  440. package/dist/dev/skins/dist/default/minimal/tailwind/components/seek.js.map +1 -1
  441. package/dist/dev/skins/dist/default/minimal/tailwind/components/slider.js +5 -3
  442. package/dist/dev/skins/dist/default/minimal/tailwind/components/slider.js.map +1 -1
  443. package/dist/dev/skins/dist/default/minimal/tailwind/components/thumbnail.js +13 -0
  444. package/dist/dev/skins/dist/default/minimal/tailwind/components/thumbnail.js.map +1 -0
  445. package/dist/dev/skins/dist/default/minimal/tailwind/components/time.js +1 -1
  446. package/dist/dev/skins/dist/default/minimal/tailwind/components/time.js.map +1 -1
  447. package/dist/dev/skins/dist/default/minimal/tailwind/video.tailwind.js +27 -11
  448. package/dist/dev/skins/dist/default/minimal/tailwind/video.tailwind.js.map +1 -1
  449. package/dist/dev/skins/dist/default/shared/tailwind/icon-state.js +17 -2
  450. package/dist/dev/skins/dist/default/shared/tailwind/icon-state.js.map +1 -1
  451. package/dist/dev/tailwind.css +0 -24
  452. package/dist/dev/ui/airplay-button/airplay-button.d.ts +15 -0
  453. package/dist/dev/ui/airplay-button/airplay-button.d.ts.map +1 -0
  454. package/dist/dev/ui/airplay-button/airplay-button.js +17 -0
  455. package/dist/dev/ui/airplay-button/airplay-button.js.map +1 -0
  456. package/dist/dev/ui/airplay-button/index.d.ts +2 -0
  457. package/dist/dev/ui/airplay-button/index.js +2 -0
  458. package/dist/dev/ui/alert-dialog/alert-dialog-close.js +1 -1
  459. package/dist/dev/ui/alert-dialog/alert-dialog-popup.js +1 -1
  460. package/dist/dev/ui/audio-track/index.d.ts +2 -0
  461. package/dist/dev/ui/audio-track/index.js +2 -0
  462. package/dist/dev/ui/audio-track/use-audio-track-options.d.ts +25 -0
  463. package/dist/dev/ui/audio-track/use-audio-track-options.d.ts.map +1 -0
  464. package/dist/dev/ui/audio-track/use-audio-track-options.js +34 -0
  465. package/dist/dev/ui/audio-track/use-audio-track-options.js.map +1 -0
  466. package/dist/dev/ui/captions-radio-group/index.d.ts +2 -0
  467. package/dist/dev/ui/captions-radio-group/index.js +2 -0
  468. package/dist/dev/ui/captions-radio-group/use-captions-options.d.ts +26 -0
  469. package/dist/dev/ui/captions-radio-group/use-captions-options.d.ts.map +1 -0
  470. package/dist/dev/ui/captions-radio-group/use-captions-options.js +40 -0
  471. package/dist/dev/ui/captions-radio-group/use-captions-options.js.map +1 -0
  472. package/dist/dev/ui/create-media-button.js +23 -10
  473. package/dist/dev/ui/create-media-button.js.map +1 -1
  474. package/dist/dev/ui/error-dialog/error-dialog-description.js +1 -1
  475. package/dist/dev/ui/hooks/use-button.d.ts +2 -1
  476. package/dist/dev/ui/hooks/use-button.d.ts.map +1 -1
  477. package/dist/dev/ui/hooks/use-button.js.map +1 -1
  478. package/dist/dev/ui/hotkey/use-hotkey-shortcut.d.ts +7 -0
  479. package/dist/dev/ui/hotkey/use-hotkey-shortcut.d.ts.map +1 -0
  480. package/dist/dev/ui/hotkey/use-hotkey-shortcut.js +28 -0
  481. package/dist/dev/ui/hotkey/use-hotkey-shortcut.js.map +1 -0
  482. package/dist/dev/ui/hotkey/use-hotkey.js +1 -2
  483. package/dist/dev/ui/hotkey/use-hotkey.js.map +1 -1
  484. package/dist/dev/ui/input-indicators/use-input-indicator-root.js +2 -2
  485. package/dist/dev/ui/input-indicators/use-input-indicator-root.js.map +1 -1
  486. package/dist/dev/ui/input-indicators/use-rendered-indicator-state.js +7 -4
  487. package/dist/dev/ui/input-indicators/use-rendered-indicator-state.js.map +1 -1
  488. package/dist/dev/ui/live-button/live-button.js +1 -1
  489. package/dist/dev/ui/live-button/live-button.js.map +1 -1
  490. package/dist/dev/ui/menu/context.d.ts.map +1 -1
  491. package/dist/dev/ui/menu/context.js +16 -1
  492. package/dist/dev/ui/menu/context.js.map +1 -1
  493. package/dist/dev/ui/menu/index.parts.d.ts +3 -2
  494. package/dist/dev/ui/menu/index.parts.js +4 -2
  495. package/dist/dev/ui/menu/index.parts.js.map +1 -1
  496. package/dist/dev/ui/menu/menu-back.d.ts.map +1 -1
  497. package/dist/dev/ui/menu/menu-back.js +0 -1
  498. package/dist/dev/ui/menu/menu-back.js.map +1 -1
  499. package/dist/dev/ui/menu/menu-checkbox-item.d.ts.map +1 -1
  500. package/dist/dev/ui/menu/menu-checkbox-item.js +1 -2
  501. package/dist/dev/ui/menu/menu-checkbox-item.js.map +1 -1
  502. package/dist/dev/ui/menu/menu-content.d.ts.map +1 -1
  503. package/dist/dev/ui/menu/menu-content.js +23 -5
  504. package/dist/dev/ui/menu/menu-content.js.map +1 -1
  505. package/dist/dev/ui/menu/menu-group-label.d.ts +15 -0
  506. package/dist/dev/ui/menu/menu-group-label.d.ts.map +1 -0
  507. package/dist/dev/ui/menu/menu-group-label.js +29 -0
  508. package/dist/dev/ui/menu/menu-group-label.js.map +1 -0
  509. package/dist/dev/ui/menu/menu-group.d.ts +1 -4
  510. package/dist/dev/ui/menu/menu-group.d.ts.map +1 -1
  511. package/dist/dev/ui/menu/menu-group.js +7 -9
  512. package/dist/dev/ui/menu/menu-group.js.map +1 -1
  513. package/dist/dev/ui/menu/menu-item-indicator.d.ts.map +1 -1
  514. package/dist/dev/ui/menu/menu-item-indicator.js +1 -2
  515. package/dist/dev/ui/menu/menu-item-indicator.js.map +1 -1
  516. package/dist/dev/ui/menu/menu-item-setting-provider.js +72 -0
  517. package/dist/dev/ui/menu/menu-item-setting-provider.js.map +1 -0
  518. package/dist/dev/ui/menu/menu-item-type.d.ts +5 -0
  519. package/dist/dev/ui/menu/menu-item-type.d.ts.map +1 -0
  520. package/dist/dev/ui/menu/menu-item-value.d.ts +14 -0
  521. package/dist/dev/ui/menu/menu-item-value.d.ts.map +1 -0
  522. package/dist/dev/ui/menu/menu-item-value.js +28 -0
  523. package/dist/dev/ui/menu/menu-item-value.js.map +1 -0
  524. package/dist/dev/ui/menu/menu-item.d.ts +3 -0
  525. package/dist/dev/ui/menu/menu-item.d.ts.map +1 -1
  526. package/dist/dev/ui/menu/menu-item.js +44 -23
  527. package/dist/dev/ui/menu/menu-item.js.map +1 -1
  528. package/dist/dev/ui/menu/menu-radio-group.d.ts +0 -2
  529. package/dist/dev/ui/menu/menu-radio-group.d.ts.map +1 -1
  530. package/dist/dev/ui/menu/menu-radio-group.js +6 -9
  531. package/dist/dev/ui/menu/menu-radio-group.js.map +1 -1
  532. package/dist/dev/ui/menu/menu-radio-item.d.ts.map +1 -1
  533. package/dist/dev/ui/menu/menu-radio-item.js +1 -2
  534. package/dist/dev/ui/menu/menu-radio-item.js.map +1 -1
  535. package/dist/dev/ui/menu/menu-root.js +1 -1
  536. package/dist/dev/ui/menu/menu-separator.d.ts.map +1 -1
  537. package/dist/dev/ui/menu/menu-separator.js +1 -2
  538. package/dist/dev/ui/menu/menu-separator.js.map +1 -1
  539. package/dist/dev/ui/menu/menu-trigger.d.ts +5 -2
  540. package/dist/dev/ui/menu/menu-trigger.d.ts.map +1 -1
  541. package/dist/dev/ui/menu/menu-trigger.js +85 -29
  542. package/dist/dev/ui/menu/menu-trigger.js.map +1 -1
  543. package/dist/dev/ui/menu/use-menu-group.js +31 -0
  544. package/dist/dev/ui/menu/use-menu-group.js.map +1 -0
  545. package/dist/dev/ui/playback-rate/index.d.ts +2 -0
  546. package/dist/dev/ui/playback-rate/index.js +2 -0
  547. package/dist/dev/ui/playback-rate/use-playback-rate-options.d.ts +28 -0
  548. package/dist/dev/ui/playback-rate/use-playback-rate-options.d.ts.map +1 -0
  549. package/dist/dev/ui/playback-rate/use-playback-rate-options.js +37 -0
  550. package/dist/dev/ui/playback-rate/use-playback-rate-options.js.map +1 -0
  551. package/dist/dev/ui/playback-rate-button/playback-rate-button.d.ts.map +1 -1
  552. package/dist/dev/ui/playback-rate-button/playback-rate-button.js +2 -1
  553. package/dist/dev/ui/playback-rate-button/playback-rate-button.js.map +1 -1
  554. package/dist/dev/ui/poster/poster.d.ts.map +1 -1
  555. package/dist/dev/ui/poster/poster.js +17 -3
  556. package/dist/dev/ui/poster/poster.js.map +1 -1
  557. package/dist/dev/ui/quality/index.d.ts +2 -0
  558. package/dist/dev/ui/quality/index.js +2 -0
  559. package/dist/dev/ui/quality/use-quality-options.d.ts +27 -0
  560. package/dist/dev/ui/quality/use-quality-options.d.ts.map +1 -0
  561. package/dist/dev/ui/quality/use-quality-options.js +40 -0
  562. package/dist/dev/ui/quality/use-quality-options.js.map +1 -0
  563. package/dist/dev/ui/seek-button/seek-button.d.ts.map +1 -1
  564. package/dist/dev/ui/seek-button/seek-button.js +3 -1
  565. package/dist/dev/ui/seek-button/seek-button.js.map +1 -1
  566. package/dist/dev/ui/time/time-value.d.ts.map +1 -1
  567. package/dist/dev/ui/time/time-value.js +32 -5
  568. package/dist/dev/ui/time/time-value.js.map +1 -1
  569. package/dist/dev/ui/time-slider/time-slider-root.d.ts.map +1 -1
  570. package/dist/dev/ui/time-slider/time-slider-root.js +18 -6
  571. package/dist/dev/ui/time-slider/time-slider-root.js.map +1 -1
  572. package/dist/dev/ui/tooltip/context.d.ts +7 -3
  573. package/dist/dev/ui/tooltip/context.d.ts.map +1 -1
  574. package/dist/dev/ui/tooltip/context.js.map +1 -1
  575. package/dist/dev/ui/tooltip/index.d.ts +2 -2
  576. package/dist/dev/ui/tooltip/index.parts.d.ts +3 -1
  577. package/dist/dev/ui/tooltip/index.parts.js +4 -0
  578. package/dist/dev/ui/tooltip/index.parts.js.map +1 -1
  579. package/dist/dev/ui/tooltip/tooltip-arrow.js +1 -1
  580. package/dist/dev/ui/tooltip/tooltip-label.d.ts +15 -0
  581. package/dist/dev/ui/tooltip/tooltip-label.d.ts.map +1 -0
  582. package/dist/dev/ui/tooltip/tooltip-label.js +24 -0
  583. package/dist/dev/ui/tooltip/tooltip-label.js.map +1 -0
  584. package/dist/dev/ui/tooltip/tooltip-popup.d.ts.map +1 -1
  585. package/dist/dev/ui/tooltip/tooltip-popup.js +7 -3
  586. package/dist/dev/ui/tooltip/tooltip-popup.js.map +1 -1
  587. package/dist/dev/ui/tooltip/tooltip-root.js +1 -1
  588. package/dist/dev/ui/tooltip/tooltip-root.js.map +1 -1
  589. package/dist/dev/ui/tooltip/tooltip-shortcut.d.ts +15 -0
  590. package/dist/dev/ui/tooltip/tooltip-shortcut.d.ts.map +1 -0
  591. package/dist/dev/ui/tooltip/tooltip-shortcut.js +25 -0
  592. package/dist/dev/ui/tooltip/tooltip-shortcut.js.map +1 -0
  593. package/dist/dev/ui/volume-indicator/volume-indicator-root.d.ts.map +1 -1
  594. package/dist/dev/ui/volume-indicator/volume-indicator-root.js +1 -1
  595. package/dist/dev/ui/volume-indicator/volume-indicator-root.js.map +1 -1
  596. package/dist/dev/utils/use-attach-iframe.js +14 -0
  597. package/dist/dev/utils/use-attach-iframe.js.map +1 -0
  598. package/dist/dev/utils/use-media-instance.d.ts +7 -2
  599. package/dist/dev/utils/use-media-instance.d.ts.map +1 -1
  600. package/dist/dev/utils/use-media-instance.js +11 -3
  601. package/dist/dev/utils/use-media-instance.js.map +1 -1
  602. package/dist/dev/utils/use-render.d.ts.map +1 -1
  603. package/dist/dev/utils/use-render.js +1 -1
  604. package/dist/dev/utils/use-render.js.map +1 -1
  605. package/docs/README.md +1 -1
  606. package/docs/concepts/browser-support.md +1 -1
  607. package/docs/concepts/cast.md +162 -0
  608. package/docs/concepts/features.md +1 -1
  609. package/docs/concepts/overview.md +4 -4
  610. package/docs/concepts/presets.md +32 -6
  611. package/docs/concepts/skins.md +903 -318
  612. package/docs/concepts/why-videojs.md +1 -1
  613. package/docs/how-to/build-with-ai.md +7 -7
  614. package/docs/how-to/build-your-own-component.md +3 -3
  615. package/docs/how-to/customize-skins.md +2721 -847
  616. package/docs/how-to/installation.md +10 -10
  617. package/docs/llms.txt +23 -1
  618. package/docs/reference/airplay-button.md +264 -0
  619. package/docs/reference/audio-track-radio-group.md +59 -0
  620. package/docs/reference/background-video.md +76 -0
  621. package/docs/reference/captions-button.md +16 -0
  622. package/docs/reference/cast-button.md +236 -0
  623. package/docs/reference/dash-video.md +138 -0
  624. package/docs/reference/feature-audio-track.md +60 -0
  625. package/docs/reference/feature-fullscreen.md +20 -3
  626. package/docs/reference/feature-orientation-lock.md +35 -0
  627. package/docs/reference/feature-quality.md +70 -0
  628. package/docs/reference/feature-remote-playback.md +3 -1
  629. package/docs/reference/feature-text-tracks.md +10 -0
  630. package/docs/reference/hlsjs-video.md +274 -0
  631. package/docs/reference/menu.md +1194 -0
  632. package/docs/reference/mux-audio.md +218 -0
  633. package/docs/reference/mux-video.md +275 -0
  634. package/docs/reference/native-hls-video.md +212 -0
  635. package/docs/reference/play-button.md +1 -1
  636. package/docs/reference/playback-rate-button.md +16 -0
  637. package/docs/reference/player-container.md +9 -3
  638. package/docs/reference/player-provider.md +3 -1
  639. package/docs/reference/quality-radio-group.md +59 -0
  640. package/docs/reference/simple-hls-audio-only.md +93 -0
  641. package/docs/reference/simple-hls-video.md +150 -0
  642. package/docs/reference/time-slider.md +12 -0
  643. package/docs/reference/time.md +24 -0
  644. package/docs/reference/tooltip.md +89 -5
  645. package/docs/reference/use-media-attach.md +2 -2
  646. package/docs/reference/use-media.md +103 -2
  647. package/docs/reference/use-player-context.md +1 -1
  648. package/docs/reference/use-player.md +16 -1
  649. package/docs/reference/use-selector.md +1 -1
  650. package/docs/reference/use-snapshot.md +3 -3
  651. package/docs/reference/volume-slider.md +1 -1
  652. package/package.json +7 -7
  653. package/dist/default/media/hls-video/index.js.map +0 -1
  654. package/dist/default/skins/dist/default/default/tailwind/components/preview.js +0 -12
  655. package/dist/default/skins/dist/default/default/tailwind/components/preview.js.map +0 -1
  656. package/dist/default/skins/dist/default/minimal/tailwind/components/preview.js +0 -13
  657. package/dist/default/skins/dist/default/minimal/tailwind/components/preview.js.map +0 -1
  658. package/dist/default/ui/hotkey/use-aria-key-shortcuts.js +0 -16
  659. package/dist/default/ui/hotkey/use-aria-key-shortcuts.js.map +0 -1
  660. package/dist/default/ui/menu/menu-label.js +0 -23
  661. package/dist/default/ui/menu/menu-label.js.map +0 -1
  662. package/dist/default/ui/playback-rate-menu/context.js +0 -20
  663. package/dist/default/ui/playback-rate-menu/context.js.map +0 -1
  664. package/dist/default/ui/playback-rate-menu/index.js +0 -3
  665. package/dist/default/ui/playback-rate-menu/index.parts.js +0 -17
  666. package/dist/default/ui/playback-rate-menu/index.parts.js.map +0 -1
  667. package/dist/default/ui/playback-rate-menu/playback-rate-menu-content.js +0 -28
  668. package/dist/default/ui/playback-rate-menu/playback-rate-menu-content.js.map +0 -1
  669. package/dist/default/ui/playback-rate-menu/playback-rate-menu-root.js +0 -36
  670. package/dist/default/ui/playback-rate-menu/playback-rate-menu-root.js.map +0 -1
  671. package/dist/default/ui/playback-rate-menu/playback-rate-menu-trigger.js +0 -51
  672. package/dist/default/ui/playback-rate-menu/playback-rate-menu-trigger.js.map +0 -1
  673. package/dist/default/ui/playback-rate-menu/use-playback-rate-menu.js +0 -27
  674. package/dist/default/ui/playback-rate-menu/use-playback-rate-menu.js.map +0 -1
  675. package/dist/dev/media/hls-video/index.d.ts +0 -15
  676. package/dist/dev/media/hls-video/index.d.ts.map +0 -1
  677. package/dist/dev/media/hls-video/index.js.map +0 -1
  678. package/dist/dev/skins/dist/default/default/tailwind/components/preview.js +0 -12
  679. package/dist/dev/skins/dist/default/default/tailwind/components/preview.js.map +0 -1
  680. package/dist/dev/skins/dist/default/minimal/tailwind/components/preview.js +0 -13
  681. package/dist/dev/skins/dist/default/minimal/tailwind/components/preview.js.map +0 -1
  682. package/dist/dev/ui/hotkey/use-aria-key-shortcuts.d.ts +0 -5
  683. package/dist/dev/ui/hotkey/use-aria-key-shortcuts.d.ts.map +0 -1
  684. package/dist/dev/ui/hotkey/use-aria-key-shortcuts.js +0 -16
  685. package/dist/dev/ui/hotkey/use-aria-key-shortcuts.js.map +0 -1
  686. package/dist/dev/ui/menu/menu-label.d.ts +0 -15
  687. package/dist/dev/ui/menu/menu-label.d.ts.map +0 -1
  688. package/dist/dev/ui/menu/menu-label.js +0 -23
  689. package/dist/dev/ui/menu/menu-label.js.map +0 -1
  690. package/dist/dev/ui/playback-rate-menu/context.js +0 -20
  691. package/dist/dev/ui/playback-rate-menu/context.js.map +0 -1
  692. package/dist/dev/ui/playback-rate-menu/index.d.ts +0 -3
  693. package/dist/dev/ui/playback-rate-menu/index.js +0 -3
  694. package/dist/dev/ui/playback-rate-menu/index.parts.d.ts +0 -12
  695. package/dist/dev/ui/playback-rate-menu/index.parts.d.ts.map +0 -1
  696. package/dist/dev/ui/playback-rate-menu/index.parts.js +0 -17
  697. package/dist/dev/ui/playback-rate-menu/index.parts.js.map +0 -1
  698. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-content.d.ts +0 -14
  699. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-content.d.ts.map +0 -1
  700. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-content.js +0 -28
  701. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-content.js.map +0 -1
  702. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-root.d.ts +0 -23
  703. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-root.d.ts.map +0 -1
  704. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-root.js +0 -39
  705. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-root.js.map +0 -1
  706. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-trigger.d.ts +0 -14
  707. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-trigger.d.ts.map +0 -1
  708. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-trigger.js +0 -51
  709. package/dist/dev/ui/playback-rate-menu/playback-rate-menu-trigger.js.map +0 -1
  710. package/dist/dev/ui/playback-rate-menu/use-playback-rate-menu.d.ts +0 -26
  711. package/dist/dev/ui/playback-rate-menu/use-playback-rate-menu.d.ts.map +0 -1
  712. package/dist/dev/ui/playback-rate-menu/use-playback-rate-menu.js +0 -27
  713. package/dist/dev/ui/playback-rate-menu/use-playback-rate-menu.js.map +0 -1
@@ -48,8 +48,8 @@ skin.css
48
48
  'use client';
49
49
 
50
50
  import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode, isValidElement } from 'react';
51
- import { CaptionsOffIcon, CaptionsOnIcon, CastEnterIcon, CastExitIcon, CheckIcon, ChevronIcon, FullscreenEnterIcon, FullscreenExitIcon, PauseIcon, PipEnterIcon, PipExitIcon, PlayIcon, RestartIcon, SeekIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons';
52
- import { createPlayer, Poster, Container, usePlayer, BufferingIndicator, CaptionsButton, CastButton, Controls, ErrorDialog, FullscreenButton, Gesture, Hotkey, Menu, MuteButton, PiPButton, PlayButton, PlaybackRateMenu, usePlaybackRateMenu, Popover, SeekButton, SeekIndicator, Slider, StatusAnnouncer, StatusIndicator, Time, TimeSlider, Tooltip, VolumeIndicator, VolumeSlider, type RenderProp } from '@videojs/react';
51
+ import { AirPlayEnterIcon, AirPlayExitIcon, CaptionsOffIcon, CaptionsOnIcon, CastEnterIcon, CastExitIcon, CheckIcon, ChevronIcon, FullscreenEnterIcon, FullscreenExitIcon, GearIcon, PauseIcon, PipEnterIcon, PipExitIcon, PlayIcon, QualityIcon, RestartIcon, SeekIcon, SpeechIcon, SpeedIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons';
52
+ import { createPlayer, Poster, Container, usePlayer, AirPlayButton, useAudioTrackOptions, BufferingIndicator, useCaptionsOptions, CastButton, Controls, ErrorDialog, FullscreenButton, Gesture, Hotkey, Menu, MuteButton, PiPButton, PlayButton, usePlaybackRateOptions, Popover, useQualityOptions, SeekButton, SeekIndicator, Slider, StatusAnnouncer, StatusIndicator, Time, TimeSlider, Tooltip, VolumeIndicator, VolumeSlider, type RenderProp } from '@videojs/react';
53
53
  import { Video, videoFeatures } from '@videojs/react/video';
54
54
  import './player.css';
55
55
 
@@ -57,20 +57,237 @@ const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictur
57
57
 
58
58
  const CENTER_STATUS_ACTIONS = ['togglePaused'] as const;
59
59
 
60
- function PlaybackRateMenuItems(): ReactNode {
61
- const { options, setValue, value } = usePlaybackRateMenu();
60
+ function MenuChevron({ flipped = false }: { flipped?: boolean }): ReactNode {
61
+ return <ChevronIcon className={`media-icon media-menu__chevron ${flipped ? 'media-icon--flipped' : undefined}`} />;
62
+ }
63
+
64
+ function SettingsMenu(): ReactNode {
65
+ const playbackRate = usePlaybackRateOptions();
66
+ const quality = useQualityOptions();
67
+ const audioTrack = useAudioTrackOptions();
68
+ const captions = useCaptionsOptions();
69
+ const hasPlaybackRate = playbackRate?.state.availability === 'available';
70
+ const hasQuality = quality?.state.availability === 'available';
71
+ const hasAudioTrack = audioTrack?.state.availability === 'available';
72
+ const hasCaptions = captions?.state.availability === 'available';
73
+
74
+ if (!hasPlaybackRate && !hasQuality && !hasAudioTrack && !hasCaptions) return null;
62
75
 
63
76
  return (
64
- <Menu.RadioGroup className="media-menu__group" value={value} onValueChange={setValue} label="Playback rate">
65
- {options.map((option) => (
66
- <Menu.RadioItem key={option.value} className="media-menu__item" value={option.value} disabled={option.disabled}>
67
- <span>{option.label}</span>
68
- <Menu.ItemIndicator checked={option.value === value} forceMount className="media-menu__indicator">
69
- <CheckIcon className="media-icon" />
70
- </Menu.ItemIndicator>
71
- </Menu.RadioItem>
72
- ))}
73
- </Menu.RadioGroup>
77
+ <Menu.Root side="top" align="center">
78
+ <Menu.Trigger aria-label="Settings" className="media-button--settings" render={<Button />}>
79
+ <GearIcon className="media-icon media-icon--settings" />
80
+ </Menu.Trigger>
81
+ <Menu.Content className="media-surface media-popover media-menu media-menu--settings">
82
+ <Menu.View className="media-menu__panel">
83
+ <div className="media-menu__group">
84
+ {hasQuality ? (
85
+ <Menu.Root>
86
+ <Menu.Trigger
87
+ type="quality"
88
+ className="media-menu__item media-menu__item--submenu"
89
+ render={(props) => (
90
+ <div {...props}>
91
+ <QualityIcon className="media-icon" />
92
+ <span>Quality</span>
93
+ <span className="media-menu__hint">
94
+ <Menu.ItemValue className="media-menu__hint-label" />
95
+ <MenuChevron />
96
+ </span>
97
+ </div>
98
+ )}
99
+ />
100
+ <Menu.Content className="media-menu__panel">
101
+ <Menu.Back className="media-menu__back">
102
+ <MenuChevron flipped />
103
+ Quality
104
+ </Menu.Back>
105
+ <Menu.Separator className="media-menu__separator" />
106
+ <Menu.RadioGroup
107
+ className="media-menu__group"
108
+ value={quality.value}
109
+ onValueChange={quality.setValue}
110
+ aria-label="Quality"
111
+ >
112
+ {quality.options.map((option) => (
113
+ <Menu.RadioItem
114
+ key={option.value}
115
+ className="media-menu__item"
116
+ value={option.value}
117
+ disabled={option.disabled}
118
+ >
119
+ <span>
120
+ {option.label}
121
+ {option.tier ? <sup className="media-menu__tier">{option.tier}</sup> : null}
122
+ </span>
123
+ {option.badge ? <span className="media-badge">{option.badge}</span> : null}
124
+ <Menu.ItemIndicator
125
+ checked={option.value === quality.value}
126
+ forceMount
127
+ className="media-menu__indicator"
128
+ >
129
+ <CheckIcon className="media-icon" />
130
+ </Menu.ItemIndicator>
131
+ </Menu.RadioItem>
132
+ ))}
133
+ </Menu.RadioGroup>
134
+ </Menu.Content>
135
+ </Menu.Root>
136
+ ) : null}
137
+
138
+ {hasAudioTrack ? (
139
+ <Menu.Root>
140
+ <Menu.Trigger
141
+ type="audio-track"
142
+ className="media-menu__item media-menu__item--submenu"
143
+ render={(props) => (
144
+ <div {...props}>
145
+ <SpeechIcon className="media-icon" />
146
+ <span>Audio</span>
147
+ <span className="media-menu__hint">
148
+ <Menu.ItemValue className="media-menu__hint-label" />
149
+ <MenuChevron />
150
+ </span>
151
+ </div>
152
+ )}
153
+ />
154
+ <Menu.Content className="media-menu__panel">
155
+ <Menu.Back className="media-menu__back">
156
+ <MenuChevron flipped />
157
+ Audio
158
+ </Menu.Back>
159
+ <Menu.Separator className="media-menu__separator" />
160
+ <Menu.RadioGroup
161
+ className="media-menu__group"
162
+ value={audioTrack.value}
163
+ onValueChange={audioTrack.setValue}
164
+ aria-label="Audio tracks"
165
+ >
166
+ {audioTrack.options.map((option) => (
167
+ <Menu.RadioItem
168
+ key={option.value}
169
+ className="media-menu__item"
170
+ value={option.value}
171
+ disabled={option.disabled}
172
+ >
173
+ <span>{option.label}</span>
174
+ <Menu.ItemIndicator
175
+ checked={option.value === audioTrack.value}
176
+ forceMount
177
+ className="media-menu__indicator"
178
+ >
179
+ <CheckIcon className="media-icon" />
180
+ </Menu.ItemIndicator>
181
+ </Menu.RadioItem>
182
+ ))}
183
+ </Menu.RadioGroup>
184
+ </Menu.Content>
185
+ </Menu.Root>
186
+ ) : null}
187
+
188
+ {hasPlaybackRate ? (
189
+ <Menu.Root>
190
+ <Menu.Trigger
191
+ type="playback-rate"
192
+ className="media-menu__item media-menu__item--submenu"
193
+ render={(props) => (
194
+ <div {...props}>
195
+ <SpeedIcon className="media-icon" />
196
+ <span>Speed</span>
197
+ <span className="media-menu__hint">
198
+ <Menu.ItemValue className="media-menu__hint-label" />
199
+ <MenuChevron />
200
+ </span>
201
+ </div>
202
+ )}
203
+ />
204
+ <Menu.Content className="media-menu__panel">
205
+ <Menu.Back className="media-menu__back">
206
+ <MenuChevron flipped />
207
+ Speed
208
+ </Menu.Back>
209
+ <Menu.Separator className="media-menu__separator" />
210
+ <Menu.RadioGroup
211
+ className="media-menu__group"
212
+ value={playbackRate.value}
213
+ onValueChange={playbackRate.setValue}
214
+ aria-label="Playback rate"
215
+ >
216
+ {playbackRate.options.map((option) => (
217
+ <Menu.RadioItem
218
+ key={option.value}
219
+ className="media-menu__item"
220
+ value={option.value}
221
+ disabled={option.disabled}
222
+ >
223
+ <span>{option.label}</span>
224
+ <Menu.ItemIndicator
225
+ checked={option.value === playbackRate.value}
226
+ forceMount
227
+ className="media-menu__indicator"
228
+ >
229
+ <CheckIcon className="media-icon" />
230
+ </Menu.ItemIndicator>
231
+ </Menu.RadioItem>
232
+ ))}
233
+ </Menu.RadioGroup>
234
+ </Menu.Content>
235
+ </Menu.Root>
236
+ ) : null}
237
+
238
+ {hasCaptions ? (
239
+ <Menu.Root>
240
+ <Menu.Trigger
241
+ type="captions"
242
+ className="media-menu__item media-menu__item--submenu"
243
+ render={(props) => (
244
+ <div {...props}>
245
+ <CaptionsOffIcon className="media-icon" />
246
+ <span>Captions</span>
247
+ <span className="media-menu__hint">
248
+ <Menu.ItemValue className="media-menu__hint-label" />
249
+ <MenuChevron />
250
+ </span>
251
+ </div>
252
+ )}
253
+ />
254
+ <Menu.Content className="media-menu__panel">
255
+ <Menu.Back className="media-menu__back">
256
+ <MenuChevron flipped />
257
+ Captions
258
+ </Menu.Back>
259
+ <Menu.Separator className="media-menu__separator" />
260
+ <Menu.RadioGroup
261
+ className="media-menu__group"
262
+ value={captions.value}
263
+ onValueChange={captions.setValue}
264
+ aria-label="Captions"
265
+ >
266
+ {captions.options.map((option) => (
267
+ <Menu.RadioItem
268
+ key={option.value}
269
+ className="media-menu__item"
270
+ value={option.value}
271
+ disabled={option.disabled}
272
+ >
273
+ <span>{option.label}</span>
274
+ <Menu.ItemIndicator
275
+ checked={option.value === captions.value}
276
+ forceMount
277
+ className="media-menu__indicator"
278
+ >
279
+ <CheckIcon className="media-icon" />
280
+ </Menu.ItemIndicator>
281
+ </Menu.RadioItem>
282
+ ))}
283
+ </Menu.RadioGroup>
284
+ </Menu.Content>
285
+ </Menu.Root>
286
+ ) : null}
287
+ </div>
288
+ </Menu.View>
289
+ </Menu.Content>
290
+ </Menu.Root>
74
291
  );
75
292
  }
76
293
 
@@ -98,10 +315,18 @@ export interface VideoPlayerProps {
98
315
  * />
99
316
  * ```
100
317
  */
101
- export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProps): ReactNode {
318
+ export function VideoPlayer({ src, className, poster, placeholder, style, ...rest }: VideoPlayerProps): ReactNode {
319
+ const containerStyle = placeholder
320
+ ? ({ '--media-poster-placeholder': `url(${placeholder})`, ...style } as CSSProperties)
321
+ : style;
322
+
102
323
  return (
103
324
  <Player.Provider>
104
- <Container className={`media-default-skin media-default-skin--video ${className ?? ''}`} {...rest}>
325
+ <Container
326
+ className={`media-default-skin media-default-skin--video ${className ?? ''}`}
327
+ style={containerStyle}
328
+ {...rest}
329
+ >
105
330
  <Video src={src} playsInline />
106
331
 
107
332
  {poster && (
@@ -111,9 +336,7 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
111
336
  <BufferingIndicator
112
337
  render={(props) => (
113
338
  <div {...props} className="media-buffering-indicator">
114
- <div className="media-surface">
115
- <SpinnerIcon className="media-icon" />
116
- </div>
339
+ <SpinnerIcon className="media-icon" />
117
340
  </div>
118
341
  )}
119
342
  />
@@ -145,7 +368,10 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
145
368
  </PlayButton>
146
369
  }
147
370
  />
148
- <Tooltip.Popup className="media-surface media-tooltip" />
371
+ <Tooltip.Popup className="media-surface media-tooltip">
372
+ <Tooltip.Label />
373
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
374
+ </Tooltip.Popup>
149
375
  </Tooltip.Root>
150
376
 
151
377
  <Tooltip.Root side="top">
@@ -159,7 +385,10 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
159
385
  </SeekButton>
160
386
  }
161
387
  />
162
- <Tooltip.Popup className="media-surface media-tooltip" />
388
+ <Tooltip.Popup className="media-surface media-tooltip">
389
+ <Tooltip.Label />
390
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
391
+ </Tooltip.Popup>
163
392
  </Tooltip.Root>
164
393
 
165
394
  <Tooltip.Root side="top">
@@ -173,7 +402,10 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
173
402
  </SeekButton>
174
403
  }
175
404
  />
176
- <Tooltip.Popup className="media-surface media-tooltip" />
405
+ <Tooltip.Popup className="media-surface media-tooltip">
406
+ <Tooltip.Label />
407
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
408
+ </Tooltip.Popup>
177
409
  </Tooltip.Root>
178
410
  </div>
179
411
 
@@ -186,47 +418,51 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
186
418
  </TimeSlider.Track>
187
419
  <TimeSlider.Thumb className="media-slider__thumb" />
188
420
 
189
- <div className="media-surface media-preview media-slider__preview">
190
- <Slider.Thumbnail className="media-preview__thumbnail" />
191
- <TimeSlider.Value type="pointer" className="media-time media-preview__time" />
192
- <SpinnerIcon className="media-preview__spinner media-icon" />
421
+ <div className="media-surface media-thumbnail media-slider__thumbnail">
422
+ <Slider.Thumbnail className="media-thumbnail__image" />
423
+ <TimeSlider.Value type="pointer" className="media-time media-thumbnail__time" />
424
+ <SpinnerIcon className="media-thumbnail__spinner media-icon" />
193
425
  </div>
426
+ <TimeSlider.Preview className="media-slider__preview">
427
+ <TimeSlider.Value type="pointer" className="media-time media-slider__value" />
428
+ </TimeSlider.Preview>
194
429
  </TimeSlider.Root>
195
- <Time.Value type="duration" className="media-time" />
430
+ <Time.Value toggle type="remaining" className="media-time" />
196
431
  </div>
197
432
 
198
433
  <div className="media-button-group">
199
- <PlaybackRateMenu.Root side="top" align="center">
200
- <PlaybackRateMenu.Trigger className="media-button--playback-rate" render={<Button />} />
201
- <PlaybackRateMenu.Content className="media-surface media-popover media-menu media-menu--playback-rate">
202
- <PlaybackRateMenuItems />
203
- </PlaybackRateMenu.Content>
204
- </PlaybackRateMenu.Root>
205
-
206
434
  <VolumePopover />
207
435
 
436
+ <SettingsMenu />
437
+
208
438
  <Tooltip.Root side="top">
209
439
  <Tooltip.Trigger
210
440
  render={
211
- <CaptionsButton className="media-button--captions" render={<Button />}>
212
- <CaptionsOffIcon className="media-icon media-icon--captions-off" />
213
- <CaptionsOnIcon className="media-icon media-icon--captions-on" />
214
- </CaptionsButton>
441
+ <CastButton className="media-button--cast" render={<Button />}>
442
+ <CastEnterIcon className="media-icon media-icon--cast-enter" />
443
+ <CastExitIcon className="media-icon media-icon--cast-exit" />
444
+ </CastButton>
215
445
  }
216
446
  />
217
- <Tooltip.Popup className="media-surface media-tooltip" />
447
+ <Tooltip.Popup className="media-surface media-tooltip">
448
+ <Tooltip.Label />
449
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
450
+ </Tooltip.Popup>
218
451
  </Tooltip.Root>
219
452
 
220
453
  <Tooltip.Root side="top">
221
454
  <Tooltip.Trigger
222
455
  render={
223
- <CastButton className="media-button--cast" render={<Button />}>
224
- <CastEnterIcon className="media-icon media-icon--cast-enter" />
225
- <CastExitIcon className="media-icon media-icon--cast-exit" />
226
- </CastButton>
456
+ <AirPlayButton className="media-button--airplay" render={<Button />}>
457
+ <AirPlayEnterIcon className="media-icon media-icon--airplay-enter" />
458
+ <AirPlayExitIcon className="media-icon media-icon--airplay-exit" />
459
+ </AirPlayButton>
227
460
  }
228
461
  />
229
- <Tooltip.Popup className="media-surface media-tooltip" />
462
+ <Tooltip.Popup className="media-surface media-tooltip">
463
+ <Tooltip.Label />
464
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
465
+ </Tooltip.Popup>
230
466
  </Tooltip.Root>
231
467
 
232
468
  <Tooltip.Root side="top">
@@ -238,7 +474,10 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
238
474
  </PiPButton>
239
475
  }
240
476
  />
241
- <Tooltip.Popup className="media-surface media-tooltip" />
477
+ <Tooltip.Popup className="media-surface media-tooltip">
478
+ <Tooltip.Label />
479
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
480
+ </Tooltip.Popup>
242
481
  </Tooltip.Root>
243
482
 
244
483
  <Tooltip.Root side="top">
@@ -250,7 +489,10 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
250
489
  </FullscreenButton>
251
490
  }
252
491
  />
253
- <Tooltip.Popup className="media-surface media-tooltip" />
492
+ <Tooltip.Popup className="media-surface media-tooltip">
493
+ <Tooltip.Label />
494
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
495
+ </Tooltip.Popup>
254
496
  </Tooltip.Root>
255
497
  </div>
256
498
  </Tooltip.Provider>
@@ -419,6 +661,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
419
661
  --media-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.15));
420
662
  --media-current-shadow-color-subtle: oklch(from var(--media-current-shadow-color) l c h / calc(alpha * 0.4));
421
663
  --media-icon-size: 18px;
664
+ --media-color-scrollbar-thumb: oklch(from currentColor l c h / 0.3);
422
665
  position: relative;
423
666
  display: block;
424
667
  width: 100%;
@@ -437,6 +680,8 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
437
680
  letter-spacing: normal;
438
681
  outline: 2px solid transparent;
439
682
  outline-offset: -4px;
683
+ scrollbar-color: var(--media-color-scrollbar-thumb) transparent;
684
+ scrollbar-width: thin;
440
685
  border-radius: var(--media-border-radius, 2rem);
441
686
  isolation: isolate;
442
687
  transition-timing-function: ease-out;
@@ -447,6 +692,16 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
447
692
  outline-color: currentColor;
448
693
  outline-offset: 2px;
449
694
  }
695
+
696
+ &::-webkit-scrollbar-thumb {
697
+ background: var(--media-color-scrollbar-thumb);
698
+ border-radius: 9999px;
699
+ }
700
+
701
+ @media (prefers-reduced-transparency: reduce) or (prefers-contrast: more) {
702
+ --media-color-scrollbar-thumb: oklch(from currentColor l c h / 0.8);
703
+ scrollbar-width: auto;
704
+ }
450
705
  }
451
706
 
452
707
  /* ==========================================================================
@@ -505,7 +760,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
505
760
  position: absolute;
506
761
  inset: 0;
507
762
  pointer-events: none;
508
- background-image: linear-gradient(to top, oklch(0 0 0 / 0.5), oklch(0 0 0 / 0.3), oklch(0 0 0 / 0));
763
+ background-image: linear-gradient(to top, oklch(0 0 0 / 0.5), oklch(0 0 0 / 0.3) 25%, oklch(0 0 0 / 0));
509
764
  border-radius: inherit;
510
765
  opacity: 0;
511
766
  backdrop-filter: blur(0) saturate(1);
@@ -524,6 +779,12 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
524
779
  opacity: 1;
525
780
  }
526
781
 
782
+ .media-default-skin .media-buffering-indicator[data-visible] ~ .media-overlay {
783
+ background: oklch(0 0 0 / 0.35);
784
+ opacity: 1;
785
+ backdrop-filter: blur(8px);
786
+ }
787
+
527
788
  .media-default-skin .media-error[data-open] ~ .media-overlay {
528
789
  backdrop-filter: blur(16px) saturate(1.5);
529
790
  }
@@ -535,9 +796,9 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
535
796
  .media-default-skin .media-buffering-indicator {
536
797
  position: absolute;
537
798
  inset: 0;
799
+ z-index: 10;
538
800
  display: none;
539
- align-items: center;
540
- justify-content: center;
801
+ place-content: center;
541
802
  color: oklch(1 0 0);
542
803
  pointer-events: none;
543
804
 
@@ -546,12 +807,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
546
807
  }
547
808
 
548
809
  &[data-visible] {
549
- display: flex;
550
- }
551
-
552
- .media-surface {
553
- padding: 0.25rem;
554
- border-radius: 100%;
810
+ display: grid;
555
811
  }
556
812
  }
557
813
 
@@ -621,6 +877,21 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
621
877
  font-variant-numeric: tabular-nums;
622
878
  }
623
879
 
880
+ .media-default-skin .media-time[role="button"] {
881
+ cursor: pointer;
882
+ outline: 2px solid transparent;
883
+ outline-offset: -2px;
884
+ border-radius: 0.25rem;
885
+ transition-timing-function: ease-out;
886
+ transition-duration: 100ms;
887
+ transition-property: outline-color, outline-offset;
888
+
889
+ &:focus-visible {
890
+ outline-color: currentColor;
891
+ outline-offset: 2px;
892
+ }
893
+ }
894
+
624
895
  /* ==========================================================================
625
896
  Buttons
626
897
  ========================================================================== */
@@ -701,6 +972,10 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
701
972
  scale: 0.9;
702
973
  }
703
974
 
975
+ & .media-icon__container {
976
+ display: grid;
977
+ }
978
+
704
979
  & .media-icon {
705
980
  grid-area: 1 / 1;
706
981
  transition-behavior: allow-discrete;
@@ -718,8 +993,9 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
718
993
  right: -1px;
719
994
  bottom: -3px;
720
995
  font-size: 10px;
721
- font-weight: 480;
996
+ font-weight: 500;
722
997
  font-variant-numeric: tabular-nums;
998
+ letter-spacing: -0.05em;
723
999
  }
724
1000
 
725
1001
  &:has(.media-icon--flipped) .media-icon__label {
@@ -743,6 +1019,27 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
743
1019
  }
744
1020
  }
745
1021
 
1022
+ /* Settings button */
1023
+ .media-default-skin .media-button--settings {
1024
+ display: none;
1025
+
1026
+ & .media-icon--settings {
1027
+ transition: transform 150ms ease-in-out;
1028
+
1029
+ @media (prefers-reduced-motion: reduce) {
1030
+ transition-duration: 0ms;
1031
+ }
1032
+ }
1033
+
1034
+ &[aria-expanded="true"] .media-icon--settings {
1035
+ transform: rotate(90deg);
1036
+ }
1037
+ }
1038
+
1039
+ .media-default-skin .media-button-group:has([data-availability="available"]) .media-button--settings {
1040
+ display: grid;
1041
+ }
1042
+
746
1043
  /* Live button — wide pill button with a status dot (gray → red at the live
747
1044
  edge) rendered via ::before, and "LIVE" text rendered as the button's own
748
1045
  text content. */
@@ -789,6 +1086,21 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
789
1086
  }
790
1087
  }
791
1088
 
1089
+ /* ==========================================================================
1090
+ Badge
1091
+ ========================================================================== */
1092
+
1093
+ .media-default-skin .media-badge {
1094
+ padding: 0.125rem 0.375rem;
1095
+ font-size: 0.6875rem;
1096
+ font-weight: 500;
1097
+ line-height: 1;
1098
+ color: oklch(from currentColor l c h / 0.85);
1099
+ white-space: nowrap;
1100
+ background-color: oklch(from currentColor l c h / 0.1);
1101
+ border-radius: calc(infinity * 1px);
1102
+ }
1103
+
792
1104
  /* ==========================================================================
793
1105
  Icons
794
1106
  ========================================================================== */
@@ -796,6 +1108,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
796
1108
  .media-default-skin .media-icon__container {
797
1109
  position: relative;
798
1110
  }
1111
+
799
1112
  .media-default-skin .media-icon {
800
1113
  flex-shrink: 0;
801
1114
  width: var(--media-icon-size);
@@ -805,102 +1118,6 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
805
1118
  scale: -1 1;
806
1119
  }
807
1120
 
808
- /* ==========================================================================
809
- Menus
810
- Note: Menus use `.media-popover` styles for positioning and transitions.
811
- ========================================================================== */
812
-
813
- .media-default-skin .media-popover.media-menu {
814
- box-sizing: border-box;
815
- min-width: min(6rem, var(--media-popover-available-width, 6rem));
816
- max-width: var(--media-popover-available-width, none);
817
- max-height: var(--media-popover-available-height, none);
818
- padding: 0.375rem;
819
- overflow: auto;
820
- overscroll-behavior: none;
821
- border-radius: 1.25rem;
822
-
823
- &::before {
824
- display: none;
825
- }
826
- }
827
-
828
- .media-default-skin .media-popover.media-menu .media-menu__group {
829
- position: relative;
830
- display: flex;
831
- flex-direction: column;
832
- gap: 0.125rem;
833
-
834
- &::before {
835
- position: absolute;
836
- position-anchor: --media-menu-item-highlight-anchor;
837
- inset: anchor(inside);
838
- pointer-events: none;
839
- content: "";
840
- background-color: oklch(from currentColor l c h / 0.1);
841
- border-radius: calc(infinity * 1px);
842
- transition: inset ease-in-out 100ms;
843
- }
844
-
845
- @supports not (top: anchor(top)) {
846
- &::before {
847
- display: none;
848
- }
849
- }
850
- }
851
-
852
- .media-default-skin .media-popover.media-menu .media-menu__item {
853
- display: flex;
854
- gap: 0.5rem;
855
- align-items: center;
856
- justify-content: space-between;
857
- min-height: 2rem;
858
- padding: 0 0.75rem;
859
- font-variant-numeric: tabular-nums;
860
- color: inherit;
861
- cursor: pointer;
862
- outline: 2px solid transparent;
863
- outline-offset: -2px;
864
- border-radius: calc(infinity * 1px);
865
-
866
- &:hover,
867
- &[data-highlighted] {
868
- anchor-name: --media-menu-item-highlight-anchor;
869
- }
870
-
871
- @supports not (top: anchor(top)) {
872
- &:hover,
873
- &[data-highlighted] {
874
- background-color: oklch(from currentColor l c h / 0.1);
875
- }
876
- }
877
-
878
- &:focus-visible {
879
- outline-color: currentColor;
880
- outline-offset: 2px;
881
- }
882
-
883
- &[aria-checked="true"] .media-menu__indicator {
884
- opacity: 1;
885
- }
886
-
887
- &[aria-disabled="true"] {
888
- pointer-events: none;
889
- cursor: not-allowed;
890
- opacity: 0.5;
891
- }
892
- }
893
-
894
- .media-default-skin .media-popover.media-menu .media-menu__indicator {
895
- flex-shrink: 0;
896
- margin-right: -0.25rem;
897
- opacity: 0;
898
- }
899
-
900
- .media-default-skin .media-popover.media-menu .media-menu__indicator .media-icon {
901
- filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
902
- }
903
-
904
1121
  /* ==========================================================================
905
1122
  Poster Image
906
1123
  ========================================================================== */
@@ -915,7 +1132,8 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
915
1132
  transition: opacity 0.25s;
916
1133
  }
917
1134
  .media-default-skin media-poster:not([data-visible]),
918
- .media-default-skin > img:not([data-visible]) {
1135
+ .media-default-skin > img:not([data-visible]),
1136
+ .media-default-skin > img[data-visible]:not([data-loaded]) {
919
1137
  opacity: 0;
920
1138
  }
921
1139
  .media-default-skin media-poster ::slotted(img),
@@ -934,6 +1152,30 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
934
1152
  border-radius: inherit;
935
1153
  }
936
1154
 
1155
+ /* Blurred placeholder: HTML path targets media-poster::before, React path targets the skin container::before */
1156
+ .media-default-skin media-poster::before,
1157
+ .media-default-skin::before {
1158
+ position: absolute;
1159
+ inset: 0;
1160
+ pointer-events: none;
1161
+ content: "";
1162
+ background-image: var(--media-poster-placeholder, none);
1163
+ background-repeat: no-repeat;
1164
+ background-position: var(--media-object-position, center);
1165
+ background-size: var(--media-object-fit, contain);
1166
+ filter: blur(var(--media-poster-placeholder-blur, 20px));
1167
+ }
1168
+
1169
+ /* React path: hidden by default, transitions in while poster loads */
1170
+ .media-default-skin::before {
1171
+ opacity: 0;
1172
+ transition: opacity 0.25s;
1173
+ }
1174
+ /* Show placeholder while the poster is visible but not yet loaded */
1175
+ .media-default-skin:has(img[data-visible]:not([data-loaded]))::before {
1176
+ opacity: 1;
1177
+ }
1178
+
937
1179
  .media-default-skin:fullscreen media-poster ::slotted(img),
938
1180
  .media-default-skin:fullscreen media-poster img,
939
1181
  .media-default-skin:fullscreen > img {
@@ -941,14 +1183,15 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
941
1183
  }
942
1184
 
943
1185
  /* ==========================================================================
944
- Media preview
1186
+ Media thumbnail
945
1187
  ========================================================================== */
946
- .media-default-skin .media-preview {
1188
+
1189
+ .media-default-skin .media-thumbnail {
947
1190
  pointer-events: none;
948
1191
  background-color: oklch(0 0 0 / 0.9);
949
1192
  border-radius: 0.75rem;
950
1193
 
951
- & .media-preview__thumbnail {
1194
+ & .media-thumbnail__image {
952
1195
  position: relative;
953
1196
  display: block;
954
1197
  overflow: clip;
@@ -963,18 +1206,14 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
963
1206
  }
964
1207
  }
965
1208
 
966
- & .media-preview__time {
1209
+ & .media-thumbnail__time {
967
1210
  position: absolute;
968
1211
  inset-inline: 0;
969
1212
  bottom: 0.5rem;
970
1213
  text-align: center;
971
1214
  }
972
1215
 
973
- & .media-overlay {
974
- opacity: 1;
975
- }
976
-
977
- & .media-preview__spinner {
1216
+ & .media-thumbnail__spinner {
978
1217
  position: absolute;
979
1218
  top: 50%;
980
1219
  left: 50%;
@@ -982,22 +1221,22 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
982
1221
  translate: -50% -50%;
983
1222
  }
984
1223
 
985
- & .media-preview__thumbnail,
986
- & .media-preview__spinner {
1224
+ & .media-thumbnail__image,
1225
+ & .media-thumbnail__spinner {
987
1226
  transition: opacity 150ms ease-out;
988
1227
  }
989
1228
 
990
- &:not(:has(.media-preview__thumbnail[data-loading])) {
991
- & .media-preview__spinner {
1229
+ &:not(:has(.media-thumbnail__image[data-loading])) {
1230
+ & .media-thumbnail__spinner {
992
1231
  --media-spinner-animation: none;
993
1232
  }
994
1233
  }
995
1234
 
996
- &:has(.media-preview__thumbnail[data-loading]) {
997
- & .media-preview__thumbnail {
1235
+ &:has(.media-thumbnail__image[data-loading]) {
1236
+ & .media-thumbnail__image {
998
1237
  opacity: 0;
999
1238
  }
1000
- & .media-preview__spinner {
1239
+ & .media-thumbnail__spinner {
1001
1240
  opacity: 1;
1002
1241
  }
1003
1242
  }
@@ -1060,9 +1299,9 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1060
1299
  background-color: currentColor;
1061
1300
  border-radius: calc(infinity * 1px);
1062
1301
  box-shadow:
1063
- 0 0 0 1px var(--media-current-shadow-color-subtle, oklch(0 0 0 / 0.1)),
1064
- 0 1px 3px 0 oklch(0 0 0 / 0.15),
1065
- 0 1px 2px -1px oklch(0 0 0 / 0.15);
1302
+ 0 0 0 1px var(--media-current-shadow-color, oklch(0 0 0 / 0.1)),
1303
+ 0 1px 3px 0 oklch(0 0 0 / 0.35),
1304
+ 0 1px 2px -1px oklch(0 0 0 / 0.35);
1066
1305
  opacity: 0;
1067
1306
  translate: -50% -50%;
1068
1307
  transition-timing-function: ease-out;
@@ -1081,7 +1320,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1081
1320
 
1082
1321
  &:hover,
1083
1322
  &:focus {
1084
- outline-color: oklch(from currentColor l c h / 0.25);
1323
+ outline-color: oklch(from currentColor l c h / 0.15);
1085
1324
  outline-offset: 0;
1086
1325
  }
1087
1326
 
@@ -1090,7 +1329,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1090
1329
  inset: -4px;
1091
1330
  content: "";
1092
1331
  border-radius: inherit;
1093
- box-shadow: 0 0 0 2px oklch(1 0 0);
1332
+ box-shadow: 0 0 0 2px currentColor;
1094
1333
  transition-timing-function: ease-out;
1095
1334
  transition-duration: 150ms;
1096
1335
  transition-property: opacity, scale;
@@ -1114,6 +1353,49 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1114
1353
  opacity: 1;
1115
1354
  }
1116
1355
 
1356
+ /* Preview */
1357
+ .media-default-skin .media-slider__preview {
1358
+ & .media-slider__value,
1359
+ &::before {
1360
+ opacity: 0;
1361
+ scale: 0.5;
1362
+ transition-timing-function: ease-out;
1363
+ transition-duration: 200ms;
1364
+ transition-property: opacity, scale;
1365
+ }
1366
+
1367
+ & .media-slider__value {
1368
+ position: absolute;
1369
+ bottom: 2.25rem;
1370
+ text-shadow: 0 1px 0 var(--media-current-shadow-color);
1371
+ filter: blur(8px);
1372
+ translate: -50% 0.5rem;
1373
+ transition-property: filter, opacity, scale, translate;
1374
+ }
1375
+
1376
+ &::before {
1377
+ display: block;
1378
+ min-width: 0.25rem;
1379
+ height: 0.25rem;
1380
+ content: "";
1381
+ background-color: currentColor;
1382
+ border-radius: 100%;
1383
+ box-shadow:
1384
+ 0 0 0 1px var(--media-current-shadow-color, oklch(0 0 0 / 0.15)),
1385
+ 0 1px 2px 0 oklch(0 0 0 / 0.35);
1386
+ }
1387
+
1388
+ &[data-pointing] .media-slider__value,
1389
+ &[data-pointing]:not([data-dragging])::before {
1390
+ opacity: 1;
1391
+ scale: 1;
1392
+ }
1393
+ &[data-pointing] .media-slider__value {
1394
+ filter: blur(0);
1395
+ translate: -50% 0;
1396
+ }
1397
+ }
1398
+
1117
1399
  /* Shared track fills */
1118
1400
  .media-default-skin .media-slider__buffer,
1119
1401
  .media-default-skin .media-slider__fill {
@@ -1185,38 +1467,52 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1185
1467
  Popups & Tooltips
1186
1468
  ========================================================================== */
1187
1469
 
1470
+ .media-default-skin {
1471
+ --media-popup-transition:
1472
+ opacity var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
1473
+ filter var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
1474
+ transform var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
1475
+ scale var(--media-popup-transition-timing-function) var(--media-popup-transition-duration);
1476
+ }
1477
+
1188
1478
  .media-default-skin .media-popover,
1189
1479
  .media-default-skin .media-tooltip {
1480
+ --popup-translate-distance: 0.5rem;
1190
1481
  margin: 0;
1191
1482
  overflow: visible;
1192
1483
  color: inherit;
1193
1484
  border: 0;
1194
- filter: blur(0px);
1195
- transition-timing-function: var(--media-popup-transition-timing-function);
1196
- transition-duration: var(--media-popup-transition-duration);
1197
- transition-property: scale, opacity, filter;
1485
+ transition: var(--media-popup-transition);
1198
1486
 
1199
1487
  &[data-starting-style],
1200
1488
  &[data-ending-style] {
1201
1489
  opacity: 0;
1202
- filter: blur(8px);
1203
- scale: 0.85;
1490
+ filter: blur(4px);
1491
+ /* We have to use transform here for translate as the translate property is used for positioning by core. */
1492
+ transform: translate(var(--popup-translate-x-distance, 0), var(--popup-translate-y-distance, 0));
1493
+ scale: 0.95;
1204
1494
  }
1205
1495
 
1206
- &[data-instant] {
1207
- transition-duration: 0ms;
1496
+ &[data-ending-style] {
1497
+ transform: none;
1498
+ /* Speed up the exit transition. */
1499
+ transition-duration: max(0ms, calc(var(--media-popup-transition-duration) - 50ms));
1208
1500
  }
1209
1501
 
1210
1502
  &[data-side="top"] {
1503
+ --popup-translate-y-distance: var(--popup-translate-distance);
1211
1504
  transform-origin: bottom;
1212
1505
  }
1213
1506
  &[data-side="bottom"] {
1507
+ --popup-translate-y-distance: calc(var(--popup-translate-distance) * -1);
1214
1508
  transform-origin: top;
1215
1509
  }
1216
1510
  &[data-side="left"] {
1511
+ --popup-translate-x-distance: var(--popup-translate-distance);
1217
1512
  transform-origin: right;
1218
1513
  }
1219
1514
  &[data-side="right"] {
1515
+ --popup-translate-x-distance: calc(var(--popup-translate-distance) * -1);
1220
1516
  transform-origin: left;
1221
1517
  }
1222
1518
 
@@ -1277,6 +1573,13 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1277
1573
  white-space: nowrap;
1278
1574
  border-radius: calc(infinity * 1px);
1279
1575
 
1576
+ /* `display: flex` must not apply while closed — it overrides UA `[popover]` hiding. */
1577
+ &[data-open] {
1578
+ display: flex;
1579
+ column-gap: 0.25rem;
1580
+ align-items: center;
1581
+ }
1582
+
1280
1583
  &[data-side="top"]::before,
1281
1584
  &[data-side="bottom"]::before {
1282
1585
  height: var(--media-tooltip-side-offset);
@@ -1285,48 +1588,292 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1285
1588
  &[data-side="right"]::before {
1286
1589
  width: var(--media-tooltip-side-offset);
1287
1590
  }
1591
+
1592
+ & .media-tooltip__kbd {
1593
+ min-width: 1.5em;
1594
+ padding: 0.1em;
1595
+ font-family: inherit;
1596
+ font-size: 90%;
1597
+ font-weight: 600;
1598
+ line-height: 1.25;
1599
+ text-align: center;
1600
+ background-color: oklch(from currentColor l c h / 0.3);
1601
+ border-radius: 0.25rem;
1602
+ }
1288
1603
  }
1289
1604
 
1290
1605
  /* ==========================================================================
1291
- Native Caption Track
1606
+ Menus
1607
+ Note: Menus use `.media-popover` styles for positioning and transitions.
1292
1608
  ========================================================================== */
1293
1609
 
1294
- .media-default-skin {
1295
- --media-caption-track-duration: var(--media-controls-transition-duration);
1296
- --media-caption-track-delay: 25ms;
1297
- --media-caption-track-y: -0.5rem;
1610
+ .media-default-skin .media-menu {
1611
+ --menu-transition-duration: 250ms;
1612
+ --menu-max-height: 14rem;
1613
+ --menu-padding: 0.25rem;
1614
+ --menu-border-radius: 0.75rem;
1615
+ --menu-item-border-radius: calc(var(--menu-border-radius) - var(--menu-padding));
1616
+ box-sizing: border-box;
1617
+ max-width: var(--media-popover-available-width, none);
1618
+ max-height: min(var(--media-popover-available-height, var(--menu-max-height)), var(--menu-max-height));
1619
+ padding: var(--menu-padding);
1620
+ overflow: auto;
1621
+ overscroll-behavior: none;
1622
+ border-radius: var(--menu-border-radius);
1298
1623
 
1299
- &:has(.media-controls[data-visible]) {
1300
- --media-caption-track-y: -5.5rem;
1624
+ @media (prefers-reduced-motion: reduce) {
1625
+ --menu-transition-duration: 0ms;
1301
1626
  }
1302
1627
 
1303
- @container media-root (width > 42rem) {
1304
- &:has(.media-controls[data-visible]) > * {
1305
- --media-caption-track-y: -3.5rem;
1628
+ & .media-menu__panel {
1629
+ position: absolute;
1630
+ inset: 0;
1631
+ padding: var(--menu-padding);
1632
+ overflow: auto;
1633
+ overscroll-behavior: none;
1634
+ outline: none;
1635
+ translate: 0 0;
1636
+ transition-timing-function: ease-in-out;
1637
+ transition-duration: var(--menu-transition-duration);
1638
+ transition-property: translate, filter;
1639
+ will-change: translate, filter;
1640
+
1641
+ &[data-starting-style],
1642
+ &[data-ending-style] {
1643
+ overflow: hidden;
1306
1644
  }
1307
- }
1308
- }
1309
1645
 
1310
- .media-default-skin video::-webkit-media-text-track-container {
1311
- z-index: 1;
1312
- font-family: inherit;
1313
- scale: 0.98;
1314
- translate: 0 var(--media-caption-track-y);
1315
- transition: translate var(--media-caption-track-duration) ease-out;
1316
- transition-delay: var(--media-caption-track-delay);
1317
- }
1646
+ /* Root settings view — slides out when a submenu is active */
1647
+ &[data-menu-root-view][data-menu-view-state="inactive"] {
1648
+ filter: blur(8px);
1649
+ translate: -100% 0;
1318
1650
 
1319
- /* ==========================================================================
1320
- Input Feedback
1321
- ========================================================================== */
1651
+ /* Hide the indicator while transitioning */
1652
+ & .media-menu__group::before {
1653
+ display: none;
1654
+ }
1655
+ }
1322
1656
 
1323
- .media-default-skin .media-input-feedback {
1324
- position: absolute;
1325
- inset-inline: 0;
1326
- top: 0;
1327
- bottom: 3.5rem; /* Shift up a little in smaller containers */
1328
- display: grid;
1329
- grid-template-columns: 1fr 1fr 1fr;
1657
+ /* Submenu panels — slide in/out alongside the root view */
1658
+ &[data-submenu] {
1659
+ z-index: 10;
1660
+
1661
+ &:not([data-open], [data-ending-style]) {
1662
+ translate: -100% 0;
1663
+ transition-property: none;
1664
+ }
1665
+
1666
+ &[data-starting-style],
1667
+ &[data-ending-style] {
1668
+ pointer-events: none;
1669
+ filter: blur(8px);
1670
+ }
1671
+
1672
+ &[data-starting-style][data-direction="forward"],
1673
+ &[data-ending-style][data-direction="back"] {
1674
+ translate: 100% 0;
1675
+ }
1676
+
1677
+ &[data-ending-style][data-direction="forward"],
1678
+ &[data-starting-style][data-direction="back"] {
1679
+ translate: -100% 0;
1680
+ }
1681
+ }
1682
+ }
1683
+
1684
+ & .media-menu__separator {
1685
+ margin-block: 0.25rem;
1686
+ border-bottom: 1px solid oklch(0 0 0 / 0.1);
1687
+ box-shadow: 0 1px 0 0 oklch(1 0 0 / 0.075);
1688
+ }
1689
+
1690
+ & .media-menu__group {
1691
+ display: flex;
1692
+ flex-direction: column;
1693
+ gap: 0.125rem;
1694
+
1695
+ @supports (top: anchor(top)) {
1696
+ &::before {
1697
+ position: absolute;
1698
+ position-anchor: --media-menu-item-highlight-anchor;
1699
+ inset: anchor(inside);
1700
+ pointer-events: none;
1701
+ content: "";
1702
+ background-color: oklch(from currentColor l c h / 0.1);
1703
+ border-radius: var(--menu-item-border-radius);
1704
+ transition: inset ease-in-out 100ms;
1705
+ }
1706
+ }
1707
+ }
1708
+
1709
+ & .media-menu__item,
1710
+ & .media-menu__back {
1711
+ display: flex;
1712
+ gap: 0.375rem;
1713
+ align-items: center;
1714
+ padding: 0.375rem 0.5rem;
1715
+ text-align: left;
1716
+ text-shadow: 0 1px 0 var(--media-current-shadow-color);
1717
+ cursor: pointer;
1718
+ user-select: none;
1719
+ outline: 2px solid transparent;
1720
+ outline-offset: -2px;
1721
+ border-radius: var(--menu-item-border-radius);
1722
+ transition: background-color, color;
1723
+ transition-timing-function: ease-out;
1724
+ transition-duration: 100ms;
1725
+
1726
+ &:focus-visible {
1727
+ outline-color: currentColor;
1728
+ outline-offset: 2px;
1729
+ }
1730
+
1731
+ &:hover,
1732
+ &[data-highlighted] {
1733
+ background-color: oklch(from currentColor l c h / 0.1);
1734
+
1735
+ @supports (top: anchor(top)) {
1736
+ anchor-name: --media-menu-item-highlight-anchor;
1737
+ background-color: transparent;
1738
+ }
1739
+ }
1740
+
1741
+ & .media-icon {
1742
+ flex-shrink: 0;
1743
+ color: oklch(from currentColor l c h / 0.65);
1744
+ filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
1745
+ }
1746
+ }
1747
+
1748
+ & .media-menu__indicator {
1749
+ flex-shrink: 0;
1750
+ margin-right: -0.25rem;
1751
+ margin-left: auto;
1752
+ opacity: 0;
1753
+
1754
+ & .media-icon {
1755
+ filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
1756
+ }
1757
+ }
1758
+
1759
+ & .media-menu__item {
1760
+ justify-content: space-between;
1761
+ font-variant-numeric: tabular-nums;
1762
+ color: inherit;
1763
+
1764
+ &[aria-disabled="true"] {
1765
+ pointer-events: none;
1766
+ cursor: not-allowed;
1767
+ opacity: 0.5;
1768
+ }
1769
+
1770
+ &[aria-checked="true"] .media-menu__indicator {
1771
+ opacity: 1;
1772
+ }
1773
+
1774
+ &[data-availability="unavailable"],
1775
+ &[data-availability="unsupported"] {
1776
+ display: none;
1777
+ }
1778
+ }
1779
+
1780
+ & .media-menu__tier {
1781
+ padding-top: 1px;
1782
+ padding-left: 0.125rem;
1783
+ font-size: 0.5625rem;
1784
+ font-weight: 600;
1785
+ line-height: 1;
1786
+ color: oklch(from currentColor l c h / 0.7);
1787
+ }
1788
+
1789
+ & .media-menu__back {
1790
+ width: 100%;
1791
+ margin-bottom: 0.125rem;
1792
+ }
1793
+
1794
+ & .media-menu__hint {
1795
+ display: inline-flex;
1796
+ gap: 0.25rem;
1797
+ align-items: center;
1798
+ min-width: 0;
1799
+ padding-left: 0.5rem;
1800
+ margin-left: auto;
1801
+ font-size: 0.75rem;
1802
+ color: oklch(from currentColor l c h / 0.65);
1803
+ }
1804
+
1805
+ & .media-menu__hint-label {
1806
+ max-width: 6rem;
1807
+ overflow: hidden;
1808
+ text-overflow: ellipsis;
1809
+ white-space: nowrap;
1810
+ }
1811
+
1812
+ & .media-menu__chevron {
1813
+ width: 0.875rem;
1814
+ height: 0.875rem;
1815
+ }
1816
+
1817
+ /* Settings menu */
1818
+ &.media-menu--settings {
1819
+ width: var(--media-menu-width);
1820
+ min-width: 12rem;
1821
+ height: var(--media-menu-height);
1822
+ overflow: hidden;
1823
+ /* Add height and width transitions. */
1824
+ transition:
1825
+ var(--media-popup-transition),
1826
+ height var(--media-popup-transition-timing-function) var(--menu-transition-duration),
1827
+ width var(--media-popup-transition-timing-function) var(--menu-transition-duration);
1828
+
1829
+ /* Don't transition width and height on open/close. */
1830
+ &[data-starting-style],
1831
+ &[data-ending-style] {
1832
+ transition: var(--media-popup-transition);
1833
+ }
1834
+ }
1835
+ }
1836
+
1837
+ /* ==========================================================================
1838
+ Native Caption Track
1839
+ ========================================================================== */
1840
+
1841
+ .media-default-skin {
1842
+ --media-caption-track-duration: var(--media-controls-transition-duration);
1843
+ --media-caption-track-delay: 25ms;
1844
+ --media-caption-track-y: -0.5rem;
1845
+
1846
+ &:has(.media-controls[data-visible]) {
1847
+ --media-caption-track-y: -5.5rem;
1848
+ }
1849
+
1850
+ @container media-root (width > 42rem) {
1851
+ &:has(.media-controls[data-visible]) > * {
1852
+ --media-caption-track-y: -3.5rem;
1853
+ }
1854
+ }
1855
+ }
1856
+
1857
+ .media-default-skin video::-webkit-media-text-track-container {
1858
+ z-index: 1;
1859
+ font-family: inherit;
1860
+ scale: 0.98;
1861
+ translate: 0 var(--media-caption-track-y);
1862
+ transition: translate var(--media-caption-track-duration) ease-out;
1863
+ transition-delay: var(--media-caption-track-delay);
1864
+ }
1865
+
1866
+ /* ==========================================================================
1867
+ Input Feedback
1868
+ ========================================================================== */
1869
+
1870
+ .media-default-skin .media-input-feedback {
1871
+ position: absolute;
1872
+ inset-inline: 0;
1873
+ top: 0;
1874
+ bottom: 3.5rem; /* Shift up a little in smaller containers */
1875
+ display: grid;
1876
+ grid-template-columns: 1fr 1fr 1fr;
1330
1877
  align-items: center;
1331
1878
  justify-items: center;
1332
1879
  color: var(--media-color-primary, oklch(1 0 0));
@@ -1423,126 +1970,127 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1423
1970
  border-radius: inherit;
1424
1971
  transition: --media-progress-fill 200ms linear;
1425
1972
  }
1426
- }
1427
1973
 
1428
- .media-default-skin .media-input-feedback-island--volume[data-level="high"] .media-icon--volume-high,
1429
- .media-default-skin .media-input-feedback-island--volume[data-level="low"] .media-icon--volume-low,
1430
- .media-default-skin .media-input-feedback-island--volume[data-level="off"] .media-icon--volume-off {
1431
- display: block;
1432
- }
1974
+ &[data-level="high"] .media-icon--volume-high,
1975
+ &[data-level="low"] .media-icon--volume-low,
1976
+ &[data-level="off"] .media-icon--volume-off {
1977
+ display: block;
1978
+ }
1433
1979
 
1434
- .media-default-skin .media-input-feedback-island--status[data-status="captions-on"] .media-icon--captions-on,
1435
- .media-default-skin .media-input-feedback-island--status[data-status="captions-off"] .media-icon--captions-off,
1436
- .media-default-skin .media-input-feedback-island--status[data-status="fullscreen"] .media-icon--fullscreen-enter,
1437
- .media-default-skin .media-input-feedback-island--status[data-status="exit-fullscreen"] .media-icon--fullscreen-exit,
1438
- .media-default-skin .media-input-feedback-island--status[data-status="pip"] .media-icon--pip-enter,
1439
- .media-default-skin .media-input-feedback-island--status[data-status="exit-pip"] .media-icon--pip-exit {
1440
- display: block;
1980
+ @media (prefers-reduced-motion: no-preference) {
1981
+ &[data-min],
1982
+ &[data-max] {
1983
+ animation: media-shake 300ms ease-in-out;
1984
+ }
1985
+ }
1441
1986
  }
1442
1987
 
1443
- /* --- Boundary shake ------------------------------------------------------- */
1444
-
1445
- @media (prefers-reduced-motion: no-preference) {
1446
- .media-default-skin .media-input-feedback-island--volume[data-min],
1447
- .media-default-skin .media-input-feedback-island--volume[data-max] {
1448
- animation: media-shake 300ms ease-in-out;
1988
+ .media-default-skin .media-input-feedback-island--status {
1989
+ &[data-status="captions-on"] .media-icon--captions-on,
1990
+ &[data-status="captions-off"] .media-icon--captions-off,
1991
+ &[data-status="fullscreen"] .media-icon--fullscreen-enter,
1992
+ &[data-status="exit-fullscreen"] .media-icon--fullscreen-exit,
1993
+ &[data-status="pip"] .media-icon--pip-enter,
1994
+ &[data-status="exit-pip"] .media-icon--pip-exit {
1995
+ display: block;
1449
1996
  }
1450
1997
  }
1451
1998
 
1452
1999
  /* --- Bubble ---------------------------------------------------------------- */
1453
2000
 
1454
2001
  .media-default-skin .media-input-feedback-bubble {
1455
- display: flex;
1456
- flex-direction: column;
2002
+ display: grid;
1457
2003
  grid-row: 1;
1458
- grid-column: 2; /* default to center for status bubbles and undirected seeks */
1459
- align-items: center;
1460
- justify-content: center;
2004
+ grid-column: 2;
2005
+ place-content: center;
1461
2006
  padding: 1rem;
1462
- transition: opacity 250ms ease-out;
1463
-
1464
- @container media-root (width > 24rem) {
1465
- padding: 2rem;
1466
- }
2007
+ text-align: center;
1467
2008
 
1468
- &[data-starting-style],
1469
- &[data-ending-style] {
1470
- opacity: 0;
1471
- transition-timing-function: ease-in;
2009
+ /* Central bubble (play, pause) */
2010
+ &:not([data-direction]) {
2011
+ background: oklch(0 0 0 / 0.35);
2012
+ border-radius: 100%;
2013
+ backdrop-filter: blur(8px);
2014
+ transition-timing-function: ease-out;
1472
2015
  transition-duration: 200ms;
1473
- }
1474
- }
2016
+ transition-property: opacity, scale;
1475
2017
 
1476
- /* Direction placement — seek bubbles move to the side implied by their direction. */
1477
- .media-default-skin .media-input-feedback-bubble[data-direction="backward"] {
1478
- grid-column: 1;
1479
- justify-self: left;
1480
- }
2018
+ &[data-starting-style],
2019
+ &[data-ending-style] {
2020
+ opacity: 0;
2021
+ scale: 0.85;
2022
+ }
1481
2023
 
1482
- .media-default-skin .media-input-feedback-bubble:not([data-direction]) {
1483
- grid-column: 2;
1484
- transition-timing-function:
1485
- ease-out, linear(0, 0.12 1.5%, 1.35 9.7%, 2.2 13.9%, 3 19.9%, 2.7 21.8%, 0.62 37.5%, 0.96 50.9%, 1);
1486
- transition-duration: 600ms;
1487
- transition-property: opacity, scale;
2024
+ &[data-ending-style] {
2025
+ transition-timing-function: ease-in;
2026
+ transition-duration: 100ms;
2027
+ }
1488
2028
 
1489
- @media (prefers-reduced-motion: reduce) {
1490
- transition: opacity 100ms ease-out;
2029
+ @media (prefers-reduced-motion: reduce) {
2030
+ transition-duration: 50ms;
2031
+ transition-property: opacity;
2032
+ }
1491
2033
  }
1492
2034
 
1493
- &[data-starting-style],
1494
- &[data-ending-style] {
1495
- opacity: 0;
1496
- scale: 0.8;
1497
- transition-timing-function: ease-in;
1498
- transition-duration: 200ms;
2035
+ /* Directional bubbles (seek) */
2036
+ &[data-direction] {
2037
+ gap: 0.25rem;
2038
+
2039
+ @container media-root (width > 24rem) {
2040
+ padding: 1.5rem;
2041
+ }
1499
2042
  }
1500
- }
1501
2043
 
1502
- .media-default-skin .media-input-feedback-bubble[data-direction="forward"] {
1503
- grid-column: 3;
1504
- justify-self: right;
1505
- }
2044
+ &[data-direction="backward"] {
2045
+ grid-column: 1;
2046
+ justify-self: left;
2047
+ }
1506
2048
 
1507
- /* --- Bubble icons ---------------------------------------------------------- */
2049
+ &[data-direction="forward"] {
2050
+ grid-column: 3;
2051
+ justify-self: right;
2052
+ }
1508
2053
 
1509
- .media-default-skin .media-input-feedback-bubble .media-icon {
1510
- display: none;
1511
- width: 36px;
1512
- height: 36px;
1513
- }
2054
+ /* Icons */
2055
+ .media-icon {
2056
+ display: none;
2057
+ width: calc(var(--media-icon-size) * 1.5);
2058
+ height: calc(var(--media-icon-size) * 1.5);
1514
2059
 
1515
- /* seek: seek icon, flipped for backward */
1516
- .media-default-skin .media-input-feedback-bubble[data-direction] .media-icon--seek {
1517
- display: block;
1518
- }
2060
+ &.media-icon--play {
2061
+ translate: 1px 0;
2062
+ }
2063
+ }
1519
2064
 
1520
- .media-default-skin .media-input-feedback-bubble[data-direction="backward"] .media-icon--seek {
1521
- transform: scaleX(-1);
1522
- }
2065
+ &[data-direction] .media-icon--seek,
2066
+ &[data-status="pause"] .media-icon--pause,
2067
+ &[data-status="play"] .media-icon--play {
2068
+ display: block;
2069
+ }
1523
2070
 
1524
- @media (prefers-reduced-motion: no-preference) {
1525
- .media-default-skin
1526
- .media-input-feedback-bubble[data-direction="forward"]:not([data-starting-style])
1527
- .media-icon--seek {
1528
- animation: media-slide-in-forward 300ms ease-in-out;
2071
+ &[data-direction="backward"] .media-icon--seek {
2072
+ scale: -1 1;
1529
2073
  }
1530
2074
 
1531
- .media-default-skin
1532
- .media-input-feedback-bubble[data-direction="backward"]:not([data-starting-style])
2075
+ @media (prefers-reduced-motion: no-preference) {
1533
2076
  .media-icon--seek {
1534
- animation: media-slide-in-backward 300ms ease-in-out;
1535
- }
2077
+ transition-timing-function: ease-in-out;
2078
+ transition-duration: 200ms;
2079
+ transition-property: translate, opacity;
2080
+ }
1536
2081
 
1537
- .media-default-skin .media-input-feedback-island--status[data-status]:not([data-starting-style]) .media-icon,
1538
- .media-default-skin .media-input-feedback-bubble[data-status]:not([data-starting-style]) .media-icon {
1539
- animation: media-pop-in 250ms ease-out;
1540
- }
1541
- }
2082
+ &[data-starting-style] .media-icon--seek,
2083
+ &[data-ending-style] .media-icon--seek {
2084
+ opacity: 0;
2085
+ }
1542
2086
 
1543
- .media-default-skin .media-input-feedback-bubble[data-status="pause"] .media-icon--pause,
1544
- .media-default-skin .media-input-feedback-bubble[data-status="play"] .media-icon--play {
1545
- display: block;
2087
+ &[data-direction="forward"][data-starting-style] .media-icon--seek {
2088
+ translate: -60% 0;
2089
+ }
2090
+ &[data-direction="backward"][data-starting-style] .media-icon--seek {
2091
+ translate: 60% 0;
2092
+ }
2093
+ }
1546
2094
  }
1547
2095
 
1548
2096
  /* ==========================================================================
@@ -1567,6 +2115,8 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1567
2115
  .media-button--pip .media-icon--pip-exit,
1568
2116
  .media-button--cast .media-icon--cast-enter,
1569
2117
  .media-button--cast .media-icon--cast-exit,
2118
+ .media-button--airplay .media-icon--airplay-enter,
2119
+ .media-button--airplay .media-icon--airplay-exit,
1570
2120
  .media-button--captions .media-icon--captions-off,
1571
2121
  .media-button--captions .media-icon--captions-on {
1572
2122
  display: none;
@@ -1577,10 +2127,11 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1577
2127
 
1578
2128
  /* Play: ended → restart */
1579
2129
  .media-button--play[data-ended] .media-icon--restart,
1580
- /* Play: paused (not ended) → play */
2130
+ /* Play: paused or not yet started (not ended) → play */
1581
2131
  .media-button--play:not([data-ended])[data-paused] .media-icon--play,
1582
- /* Play: playing (not paused, not ended) → pause */
1583
- .media-button--play:not([data-paused]):not([data-ended]) .media-icon--pause,
2132
+ .media-button--play:not([data-ended]):not([data-started]) .media-icon--play,
2133
+ /* Play: started and not paused/ended pause */
2134
+ .media-button--play[data-started]:not([data-paused]):not([data-ended]) .media-icon--pause,
1584
2135
  /* Mute: muted → volume off */
1585
2136
  .media-button--mute[data-muted] .media-icon--volume-off,
1586
2137
  /* Mute: volume low (not muted) → volume low */
@@ -1599,6 +2150,10 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1599
2150
  .media-button--cast:not([data-cast-state="connected"]) .media-icon--cast-enter,
1600
2151
  /* Cast: connected → exit */
1601
2152
  .media-button--cast[data-cast-state="connected"] .media-icon--cast-exit,
2153
+ /* AirPlay: not connected → enter */
2154
+ .media-button--airplay:not([data-airplay-state="connected"]) .media-icon--airplay-enter,
2155
+ /* AirPlay: connected → exit */
2156
+ .media-button--airplay[data-airplay-state="connected"] .media-icon--airplay-exit,
1602
2157
  /* Captions: not active → captions off */
1603
2158
  .media-button--captions:not([data-active]) .media-icon--captions-off,
1604
2159
  /* Captions: active → captions on */
@@ -1607,6 +2162,17 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1607
2162
  opacity: 1;
1608
2163
  }
1609
2164
 
2165
+ /* --- Pause keyframe animations on inactive icons --- */
2166
+
2167
+ /* The airplay-exit SVG defines its keyframes against CSS variables (mirroring
2168
+ the spinner pattern). When the AirPlay session isn't active the SVG is
2169
+ still in the DOM — just `display: none` — so its animations would keep
2170
+ running. Set the variables to `none` to short-circuit the keyframes. */
2171
+ .media-button--airplay:not([data-airplay-state="connected"]) {
2172
+ --media-icon--airplay__fill-animation: none;
2173
+ --media-icon--airplay__triangle-animation: none;
2174
+ }
2175
+
1610
2176
  /* -------------------------------------------------------------------------- */
1611
2177
  /* Global @keyframes for all video skins (CSS & Tailwind) */
1612
2178
  /* -------------------------------------------------------------------------- */
@@ -1630,27 +2196,6 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1630
2196
  }
1631
2197
  }
1632
2198
 
1633
- @keyframes media-slide-in-forward {
1634
- from {
1635
- translate: -60% 0;
1636
- opacity: 0;
1637
- }
1638
- }
1639
-
1640
- @keyframes media-slide-in-backward {
1641
- from {
1642
- translate: 60% 0;
1643
- opacity: 0;
1644
- }
1645
- }
1646
-
1647
- @keyframes media-pop-in {
1648
- from {
1649
- scale: 0.8;
1650
- opacity: 0;
1651
- }
1652
- }
1653
-
1654
2199
  /* -------------------------------------------------------------------------- */
1655
2200
  /* Global @properties for all video skins (CSS & Tailwind) */
1656
2201
  /* -------------------------------------------------------------------------- */
@@ -1696,6 +2241,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1696
2241
  --media-tooltip-boundary-offset: 0.5rem;
1697
2242
  --media-popover-side-offset: 0.5rem;
1698
2243
  --media-popover-boundary-offset: 0.5rem;
2244
+ overflow: clip;
1699
2245
  background: oklch(0 0 0);
1700
2246
 
1701
2247
  @media (prefers-reduced-motion: reduce) {
@@ -1803,8 +2349,6 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1803
2349
  bottom: 0.5rem;
1804
2350
  z-index: 10;
1805
2351
  flex-wrap: wrap;
1806
- max-width: 56rem;
1807
- margin-inline: auto;
1808
2352
  color: var(--media-color-primary, oklch(1 0 0));
1809
2353
  transform-origin: bottom;
1810
2354
  transition-timing-function: var(--media-controls-transition-timing-function);
@@ -1887,21 +2431,33 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1887
2431
  box-shadow: 0 0 0 1px oklch(0 0 0 / 0.05);
1888
2432
  }
1889
2433
 
1890
- .media-default-skin--video .media-slider__preview {
1891
- --media-preview-max-width: 11rem;
1892
- --media-preview-padding: -1.125rem;
2434
+ .media-default-skin--video .media-slider__thumbnail {
2435
+ --media-slider-thumbnail-max-width: 11rem;
2436
+ --media-slider-thumbnail-max-height: 8rem;
2437
+ --media-slider-thumbnail-padding: -1.125rem;
1893
2438
  /**
1894
2439
  Inset is the difference between the container width and the slider (100%) width.
1895
2440
  Divided by 2 as we render the time on both sides.
1896
2441
  */
1897
- --media-preview-inset: calc((100cqi - 100%) / 2);
2442
+ --media-slider-thumbnail-inset: calc((100cqi - 100%) / 2);
1898
2443
 
1899
2444
  position: absolute;
1900
2445
  bottom: calc(100% + 1.2rem);
1901
2446
  left: clamp(
1902
- calc(var(--media-preview-max-width) / 2 + var(--media-preview-padding) - var(--media-preview-inset)),
2447
+ calc(
2448
+ var(--media-slider-thumbnail-max-width) /
2449
+ 2 +
2450
+ var(--media-slider-thumbnail-padding) -
2451
+ var(--media-slider-thumbnail-inset)
2452
+ ),
1903
2453
  var(--media-slider-pointer),
1904
- calc(100% - var(--media-preview-max-width) / 2 - var(--media-preview-padding) + var(--media-preview-inset))
2454
+ calc(
2455
+ 100% -
2456
+ var(--media-slider-thumbnail-max-width) /
2457
+ 2 -
2458
+ var(--media-slider-thumbnail-padding) +
2459
+ var(--media-slider-thumbnail-inset)
2460
+ )
1905
2461
  );
1906
2462
  pointer-events: none;
1907
2463
  opacity: 0;
@@ -1913,19 +2469,22 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
1913
2469
  transition-duration: 150ms;
1914
2470
  transition-property: scale, opacity, filter;
1915
2471
 
1916
- & .media-preview__thumbnail {
1917
- max-width: var(--media-preview-max-width);
1918
- }
1919
-
1920
- &:has(.media-preview__thumbnail[data-loading]) {
1921
- max-height: 6rem;
2472
+ & .media-thumbnail__image {
2473
+ max-width: var(--media-slider-thumbnail-max-width);
2474
+ max-height: var(--media-slider-thumbnail-max-height);
1922
2475
  }
1923
2476
  }
1924
- .media-default-skin--video .media-slider[data-pointing] .media-slider__preview:has([role="img"]:not([data-hidden])) {
2477
+ .media-default-skin--video .media-slider[data-pointing] .media-slider__thumbnail:has([role="img"]:not([data-hidden])) {
1925
2478
  opacity: 1;
1926
2479
  filter: blur(0);
1927
2480
  scale: 1;
1928
2481
  }
2482
+ .media-default-skin--video
2483
+ .media-slider__thumbnail:has([role="img"]:not([data-hidden]))
2484
+ + .media-slider__preview
2485
+ .media-slider__value {
2486
+ display: none;
2487
+ }
1929
2488
 
1930
2489
  ```
1931
2490
 
@@ -1939,16 +2498,19 @@ skin.css
1939
2498
  'use client';
1940
2499
 
1941
2500
  import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode } from 'react';
1942
- import { CheckIcon, PauseIcon, PlayIcon, RestartIcon, SeekIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons';
1943
- import { createPlayer, Container, usePlayer, ErrorDialog, Hotkey, Menu, MuteButton, PlayButton, PlaybackRateMenu, usePlaybackRateMenu, Popover, SeekButton, StatusAnnouncer, Time, TimeSlider, Tooltip, VolumeSlider, type Poster, type RenderProp } from '@videojs/react';
2501
+ import { CheckIcon, PauseIcon, PlayIcon, RestartIcon, SeekIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons';
2502
+ import { createPlayer, Container, usePlayer, BufferingIndicator, ErrorDialog, Hotkey, Menu, MuteButton, PlayButton, usePlaybackRateOptions, PlaybackRateButton, Popover, SeekButton, StatusAnnouncer, Time, TimeSlider, Tooltip, VolumeSlider, type Poster, type RenderProp } from '@videojs/react';
1944
2503
  import { Audio, audioFeatures } from '@videojs/react/audio';
1945
2504
  import './player.css';
1946
2505
 
1947
- function PlaybackRateMenuItems(): ReactNode {
1948
- const { options, setValue, value } = usePlaybackRateMenu();
2506
+ function PlaybackRateRadioGroup(): ReactNode {
2507
+ const state = usePlaybackRateOptions();
2508
+ if (!state) return null;
2509
+
2510
+ const { options, setValue, value } = state;
1949
2511
 
1950
2512
  return (
1951
- <Menu.RadioGroup className="media-menu__group" value={value} onValueChange={setValue} label="Playback rate">
2513
+ <Menu.RadioGroup className="media-menu__group" value={value} onValueChange={setValue} aria-label="Playback rate">
1952
2514
  {options.map((option) => (
1953
2515
  <Menu.RadioItem key={option.value} className="media-menu__item" value={option.value} disabled={option.disabled}>
1954
2516
  <span>{option.label}</span>
@@ -1961,6 +2523,18 @@ function PlaybackRateMenuItems(): ReactNode {
1961
2523
  );
1962
2524
  }
1963
2525
 
2526
+ function PlaybackRateTrigger(): ReactNode {
2527
+ const state = usePlaybackRateOptions();
2528
+ if (!state) return null;
2529
+
2530
+ return (
2531
+ <Menu.Trigger
2532
+ disabled={state.disabled}
2533
+ render={<PlaybackRateButton className="media-button--playback-rate" render={<Button />} />}
2534
+ />
2535
+ );
2536
+ }
2537
+
1964
2538
  // ================================================================
1965
2539
  // Player
1966
2540
  // ================================================================
@@ -2006,18 +2580,30 @@ export function AudioPlayer({ src, className, ...rest }: AudioPlayerProps): Reac
2006
2580
  <div className="media-surface media-controls">
2007
2581
  <Tooltip.Provider>
2008
2582
  <div className="media-button-group">
2009
- <Tooltip.Root side="top" boundary="viewport">
2010
- <Tooltip.Trigger
2011
- render={
2012
- <PlayButton className="media-button--play" render={<Button />}>
2013
- <RestartIcon className="media-icon media-icon--restart" />
2014
- <PlayIcon className="media-icon media-icon--play" />
2015
- <PauseIcon className="media-icon media-icon--pause" />
2016
- </PlayButton>
2017
- }
2583
+ <span className="media-button--play__wrapper">
2584
+ <BufferingIndicator
2585
+ render={(props) => (
2586
+ <div {...props} className="media-buffering-indicator">
2587
+ <SpinnerIcon className="media-icon" />
2588
+ </div>
2589
+ )}
2018
2590
  />
2019
- <Tooltip.Popup className="media-surface media-tooltip" />
2020
- </Tooltip.Root>
2591
+ <Tooltip.Root side="top" boundary="viewport">
2592
+ <Tooltip.Trigger
2593
+ render={
2594
+ <PlayButton className="media-button--play" render={<Button />}>
2595
+ <RestartIcon className="media-icon media-icon--restart" />
2596
+ <PlayIcon className="media-icon media-icon--play" />
2597
+ <PauseIcon className="media-icon media-icon--pause" />
2598
+ </PlayButton>
2599
+ }
2600
+ />
2601
+ <Tooltip.Popup className="media-surface media-tooltip">
2602
+ <Tooltip.Label />
2603
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
2604
+ </Tooltip.Popup>
2605
+ </Tooltip.Root>
2606
+ </span>
2021
2607
 
2022
2608
  <Tooltip.Root side="top" boundary="viewport">
2023
2609
  <Tooltip.Trigger
@@ -2030,7 +2616,10 @@ export function AudioPlayer({ src, className, ...rest }: AudioPlayerProps): Reac
2030
2616
  </SeekButton>
2031
2617
  }
2032
2618
  />
2033
- <Tooltip.Popup className="media-surface media-tooltip">Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
2619
+ <Tooltip.Popup className="media-surface media-tooltip">
2620
+ <Tooltip.Label />
2621
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
2622
+ </Tooltip.Popup>
2034
2623
  </Tooltip.Root>
2035
2624
 
2036
2625
  <Tooltip.Root side="top" boundary="viewport">
@@ -2044,7 +2633,10 @@ export function AudioPlayer({ src, className, ...rest }: AudioPlayerProps): Reac
2044
2633
  </SeekButton>
2045
2634
  }
2046
2635
  />
2047
- <Tooltip.Popup className="media-surface media-tooltip">Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
2636
+ <Tooltip.Popup className="media-surface media-tooltip">
2637
+ <Tooltip.Label />
2638
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
2639
+ </Tooltip.Popup>
2048
2640
  </Tooltip.Root>
2049
2641
  </div>
2050
2642
 
@@ -2056,17 +2648,20 @@ export function AudioPlayer({ src, className, ...rest }: AudioPlayerProps): Reac
2056
2648
  <TimeSlider.Buffer className="media-slider__buffer" />
2057
2649
  </TimeSlider.Track>
2058
2650
  <TimeSlider.Thumb className="media-slider__thumb" />
2651
+ <TimeSlider.Preview className="media-slider__preview">
2652
+ <TimeSlider.Value type="pointer" className="media-slider__value media-time" />
2653
+ </TimeSlider.Preview>
2059
2654
  </TimeSlider.Root>
2060
- <Time.Value type="duration" className="media-time" />
2655
+ <Time.Value toggle type="remaining" className="media-time" />
2061
2656
  </div>
2062
2657
 
2063
2658
  <div className="media-button-group">
2064
- <PlaybackRateMenu.Root side="top" align="center" boundary="viewport">
2065
- <PlaybackRateMenu.Trigger className="media-button--playback-rate" render={<Button />} />
2066
- <PlaybackRateMenu.Content className="media-surface media-popover media-menu media-menu--playback-rate">
2067
- <PlaybackRateMenuItems />
2068
- </PlaybackRateMenu.Content>
2069
- </PlaybackRateMenu.Root>
2659
+ <Menu.Root side="top" align="center" boundary="viewport">
2660
+ <PlaybackRateTrigger />
2661
+ <Menu.Content className="media-surface media-popover media-menu media-menu--playback-rate">
2662
+ <PlaybackRateRadioGroup />
2663
+ </Menu.Content>
2664
+ </Menu.Root>
2070
2665
 
2071
2666
  <VolumePopover />
2072
2667
  </div>
@@ -2177,6 +2772,7 @@ function VolumePopover(): ReactNode {
2177
2772
  --media-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.15));
2178
2773
  --media-current-shadow-color-subtle: oklch(from var(--media-current-shadow-color) l c h / calc(alpha * 0.4));
2179
2774
  --media-icon-size: 18px;
2775
+ --media-color-scrollbar-thumb: oklch(from currentColor l c h / 0.3);
2180
2776
  position: relative;
2181
2777
  display: block;
2182
2778
  width: 100%;
@@ -2195,6 +2791,8 @@ function VolumePopover(): ReactNode {
2195
2791
  letter-spacing: normal;
2196
2792
  outline: 2px solid transparent;
2197
2793
  outline-offset: -4px;
2794
+ scrollbar-color: var(--media-color-scrollbar-thumb) transparent;
2795
+ scrollbar-width: thin;
2198
2796
  border-radius: var(--media-border-radius, 2rem);
2199
2797
  isolation: isolate;
2200
2798
  transition-timing-function: ease-out;
@@ -2205,6 +2803,16 @@ function VolumePopover(): ReactNode {
2205
2803
  outline-color: currentColor;
2206
2804
  outline-offset: 2px;
2207
2805
  }
2806
+
2807
+ &::-webkit-scrollbar-thumb {
2808
+ background: var(--media-color-scrollbar-thumb);
2809
+ border-radius: 9999px;
2810
+ }
2811
+
2812
+ @media (prefers-reduced-transparency: reduce) or (prefers-contrast: more) {
2813
+ --media-color-scrollbar-thumb: oklch(from currentColor l c h / 0.8);
2814
+ scrollbar-width: auto;
2815
+ }
2208
2816
  }
2209
2817
 
2210
2818
  /* ==========================================================================
@@ -2238,9 +2846,9 @@ function VolumePopover(): ReactNode {
2238
2846
  .media-default-skin .media-buffering-indicator {
2239
2847
  position: absolute;
2240
2848
  inset: 0;
2849
+ z-index: 10;
2241
2850
  display: none;
2242
- align-items: center;
2243
- justify-content: center;
2851
+ place-content: center;
2244
2852
  color: oklch(1 0 0);
2245
2853
  pointer-events: none;
2246
2854
 
@@ -2249,12 +2857,7 @@ function VolumePopover(): ReactNode {
2249
2857
  }
2250
2858
 
2251
2859
  &[data-visible] {
2252
- display: flex;
2253
- }
2254
-
2255
- .media-surface {
2256
- padding: 0.25rem;
2257
- border-radius: 100%;
2860
+ display: grid;
2258
2861
  }
2259
2862
  }
2260
2863
 
@@ -2324,6 +2927,21 @@ function VolumePopover(): ReactNode {
2324
2927
  font-variant-numeric: tabular-nums;
2325
2928
  }
2326
2929
 
2930
+ .media-default-skin .media-time[role="button"] {
2931
+ cursor: pointer;
2932
+ outline: 2px solid transparent;
2933
+ outline-offset: -2px;
2934
+ border-radius: 0.25rem;
2935
+ transition-timing-function: ease-out;
2936
+ transition-duration: 100ms;
2937
+ transition-property: outline-color, outline-offset;
2938
+
2939
+ &:focus-visible {
2940
+ outline-color: currentColor;
2941
+ outline-offset: 2px;
2942
+ }
2943
+ }
2944
+
2327
2945
  /* ==========================================================================
2328
2946
  Buttons
2329
2947
  ========================================================================== */
@@ -2404,6 +3022,10 @@ function VolumePopover(): ReactNode {
2404
3022
  scale: 0.9;
2405
3023
  }
2406
3024
 
3025
+ & .media-icon__container {
3026
+ display: grid;
3027
+ }
3028
+
2407
3029
  & .media-icon {
2408
3030
  grid-area: 1 / 1;
2409
3031
  transition-behavior: allow-discrete;
@@ -2421,8 +3043,9 @@ function VolumePopover(): ReactNode {
2421
3043
  right: -1px;
2422
3044
  bottom: -3px;
2423
3045
  font-size: 10px;
2424
- font-weight: 480;
3046
+ font-weight: 500;
2425
3047
  font-variant-numeric: tabular-nums;
3048
+ letter-spacing: -0.05em;
2426
3049
  }
2427
3050
 
2428
3051
  &:has(.media-icon--flipped) .media-icon__label {
@@ -2446,17 +3069,38 @@ function VolumePopover(): ReactNode {
2446
3069
  }
2447
3070
  }
2448
3071
 
2449
- /* Live button — wide pill button with a status dot (gray → red at the live
2450
- edge) rendered via ::before, and "LIVE" text rendered as the button's own
2451
- text content. */
2452
- .media-default-skin .media-button--live {
2453
- display: inline-flex;
2454
- gap: 0.4rem;
2455
- align-items: center;
2456
- width: auto;
2457
- aspect-ratio: auto;
2458
- padding: 0.5rem 0.75rem;
2459
- font-size: 0.75rem;
3072
+ /* Settings button */
3073
+ .media-default-skin .media-button--settings {
3074
+ display: none;
3075
+
3076
+ & .media-icon--settings {
3077
+ transition: transform 150ms ease-in-out;
3078
+
3079
+ @media (prefers-reduced-motion: reduce) {
3080
+ transition-duration: 0ms;
3081
+ }
3082
+ }
3083
+
3084
+ &[aria-expanded="true"] .media-icon--settings {
3085
+ transform: rotate(90deg);
3086
+ }
3087
+ }
3088
+
3089
+ .media-default-skin .media-button-group:has([data-availability="available"]) .media-button--settings {
3090
+ display: grid;
3091
+ }
3092
+
3093
+ /* Live button — wide pill button with a status dot (gray → red at the live
3094
+ edge) rendered via ::before, and "LIVE" text rendered as the button's own
3095
+ text content. */
3096
+ .media-default-skin .media-button--live {
3097
+ display: inline-flex;
3098
+ gap: 0.4rem;
3099
+ align-items: center;
3100
+ width: auto;
3101
+ aspect-ratio: auto;
3102
+ padding: 0.5rem 0.75rem;
3103
+ font-size: 0.75rem;
2460
3104
  font-weight: 600;
2461
3105
  line-height: 1;
2462
3106
  text-transform: uppercase;
@@ -2492,6 +3136,21 @@ function VolumePopover(): ReactNode {
2492
3136
  }
2493
3137
  }
2494
3138
 
3139
+ /* ==========================================================================
3140
+ Badge
3141
+ ========================================================================== */
3142
+
3143
+ .media-default-skin .media-badge {
3144
+ padding: 0.125rem 0.375rem;
3145
+ font-size: 0.6875rem;
3146
+ font-weight: 500;
3147
+ line-height: 1;
3148
+ color: oklch(from currentColor l c h / 0.85);
3149
+ white-space: nowrap;
3150
+ background-color: oklch(from currentColor l c h / 0.1);
3151
+ border-radius: calc(infinity * 1px);
3152
+ }
3153
+
2495
3154
  /* ==========================================================================
2496
3155
  Icons
2497
3156
  ========================================================================== */
@@ -2499,6 +3158,7 @@ function VolumePopover(): ReactNode {
2499
3158
  .media-default-skin .media-icon__container {
2500
3159
  position: relative;
2501
3160
  }
3161
+
2502
3162
  .media-default-skin .media-icon {
2503
3163
  flex-shrink: 0;
2504
3164
  width: var(--media-icon-size);
@@ -2508,102 +3168,6 @@ function VolumePopover(): ReactNode {
2508
3168
  scale: -1 1;
2509
3169
  }
2510
3170
 
2511
- /* ==========================================================================
2512
- Menus
2513
- Note: Menus use `.media-popover` styles for positioning and transitions.
2514
- ========================================================================== */
2515
-
2516
- .media-default-skin .media-popover.media-menu {
2517
- box-sizing: border-box;
2518
- min-width: min(6rem, var(--media-popover-available-width, 6rem));
2519
- max-width: var(--media-popover-available-width, none);
2520
- max-height: var(--media-popover-available-height, none);
2521
- padding: 0.375rem;
2522
- overflow: auto;
2523
- overscroll-behavior: none;
2524
- border-radius: 1.25rem;
2525
-
2526
- &::before {
2527
- display: none;
2528
- }
2529
- }
2530
-
2531
- .media-default-skin .media-popover.media-menu .media-menu__group {
2532
- position: relative;
2533
- display: flex;
2534
- flex-direction: column;
2535
- gap: 0.125rem;
2536
-
2537
- &::before {
2538
- position: absolute;
2539
- position-anchor: --media-menu-item-highlight-anchor;
2540
- inset: anchor(inside);
2541
- pointer-events: none;
2542
- content: "";
2543
- background-color: oklch(from currentColor l c h / 0.1);
2544
- border-radius: calc(infinity * 1px);
2545
- transition: inset ease-in-out 100ms;
2546
- }
2547
-
2548
- @supports not (top: anchor(top)) {
2549
- &::before {
2550
- display: none;
2551
- }
2552
- }
2553
- }
2554
-
2555
- .media-default-skin .media-popover.media-menu .media-menu__item {
2556
- display: flex;
2557
- gap: 0.5rem;
2558
- align-items: center;
2559
- justify-content: space-between;
2560
- min-height: 2rem;
2561
- padding: 0 0.75rem;
2562
- font-variant-numeric: tabular-nums;
2563
- color: inherit;
2564
- cursor: pointer;
2565
- outline: 2px solid transparent;
2566
- outline-offset: -2px;
2567
- border-radius: calc(infinity * 1px);
2568
-
2569
- &:hover,
2570
- &[data-highlighted] {
2571
- anchor-name: --media-menu-item-highlight-anchor;
2572
- }
2573
-
2574
- @supports not (top: anchor(top)) {
2575
- &:hover,
2576
- &[data-highlighted] {
2577
- background-color: oklch(from currentColor l c h / 0.1);
2578
- }
2579
- }
2580
-
2581
- &:focus-visible {
2582
- outline-color: currentColor;
2583
- outline-offset: 2px;
2584
- }
2585
-
2586
- &[aria-checked="true"] .media-menu__indicator {
2587
- opacity: 1;
2588
- }
2589
-
2590
- &[aria-disabled="true"] {
2591
- pointer-events: none;
2592
- cursor: not-allowed;
2593
- opacity: 0.5;
2594
- }
2595
- }
2596
-
2597
- .media-default-skin .media-popover.media-menu .media-menu__indicator {
2598
- flex-shrink: 0;
2599
- margin-right: -0.25rem;
2600
- opacity: 0;
2601
- }
2602
-
2603
- .media-default-skin .media-popover.media-menu .media-menu__indicator .media-icon {
2604
- filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
2605
- }
2606
-
2607
3171
  /* ==========================================================================
2608
3172
  Slider
2609
3173
  ========================================================================== */
@@ -2661,9 +3225,9 @@ function VolumePopover(): ReactNode {
2661
3225
  background-color: currentColor;
2662
3226
  border-radius: calc(infinity * 1px);
2663
3227
  box-shadow:
2664
- 0 0 0 1px var(--media-current-shadow-color-subtle, oklch(0 0 0 / 0.1)),
2665
- 0 1px 3px 0 oklch(0 0 0 / 0.15),
2666
- 0 1px 2px -1px oklch(0 0 0 / 0.15);
3228
+ 0 0 0 1px var(--media-current-shadow-color, oklch(0 0 0 / 0.1)),
3229
+ 0 1px 3px 0 oklch(0 0 0 / 0.35),
3230
+ 0 1px 2px -1px oklch(0 0 0 / 0.35);
2667
3231
  opacity: 0;
2668
3232
  translate: -50% -50%;
2669
3233
  transition-timing-function: ease-out;
@@ -2682,7 +3246,7 @@ function VolumePopover(): ReactNode {
2682
3246
 
2683
3247
  &:hover,
2684
3248
  &:focus {
2685
- outline-color: oklch(from currentColor l c h / 0.25);
3249
+ outline-color: oklch(from currentColor l c h / 0.15);
2686
3250
  outline-offset: 0;
2687
3251
  }
2688
3252
 
@@ -2691,7 +3255,7 @@ function VolumePopover(): ReactNode {
2691
3255
  inset: -4px;
2692
3256
  content: "";
2693
3257
  border-radius: inherit;
2694
- box-shadow: 0 0 0 2px oklch(1 0 0);
3258
+ box-shadow: 0 0 0 2px currentColor;
2695
3259
  transition-timing-function: ease-out;
2696
3260
  transition-duration: 150ms;
2697
3261
  transition-property: opacity, scale;
@@ -2715,6 +3279,49 @@ function VolumePopover(): ReactNode {
2715
3279
  opacity: 1;
2716
3280
  }
2717
3281
 
3282
+ /* Preview */
3283
+ .media-default-skin .media-slider__preview {
3284
+ & .media-slider__value,
3285
+ &::before {
3286
+ opacity: 0;
3287
+ scale: 0.5;
3288
+ transition-timing-function: ease-out;
3289
+ transition-duration: 200ms;
3290
+ transition-property: opacity, scale;
3291
+ }
3292
+
3293
+ & .media-slider__value {
3294
+ position: absolute;
3295
+ bottom: 2.25rem;
3296
+ text-shadow: 0 1px 0 var(--media-current-shadow-color);
3297
+ filter: blur(8px);
3298
+ translate: -50% 0.5rem;
3299
+ transition-property: filter, opacity, scale, translate;
3300
+ }
3301
+
3302
+ &::before {
3303
+ display: block;
3304
+ min-width: 0.25rem;
3305
+ height: 0.25rem;
3306
+ content: "";
3307
+ background-color: currentColor;
3308
+ border-radius: 100%;
3309
+ box-shadow:
3310
+ 0 0 0 1px var(--media-current-shadow-color, oklch(0 0 0 / 0.15)),
3311
+ 0 1px 2px 0 oklch(0 0 0 / 0.35);
3312
+ }
3313
+
3314
+ &[data-pointing] .media-slider__value,
3315
+ &[data-pointing]:not([data-dragging])::before {
3316
+ opacity: 1;
3317
+ scale: 1;
3318
+ }
3319
+ &[data-pointing] .media-slider__value {
3320
+ filter: blur(0);
3321
+ translate: -50% 0;
3322
+ }
3323
+ }
3324
+
2718
3325
  /* Shared track fills */
2719
3326
  .media-default-skin .media-slider__buffer,
2720
3327
  .media-default-skin .media-slider__fill {
@@ -2786,38 +3393,52 @@ function VolumePopover(): ReactNode {
2786
3393
  Popups & Tooltips
2787
3394
  ========================================================================== */
2788
3395
 
3396
+ .media-default-skin {
3397
+ --media-popup-transition:
3398
+ opacity var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
3399
+ filter var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
3400
+ transform var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
3401
+ scale var(--media-popup-transition-timing-function) var(--media-popup-transition-duration);
3402
+ }
3403
+
2789
3404
  .media-default-skin .media-popover,
2790
3405
  .media-default-skin .media-tooltip {
3406
+ --popup-translate-distance: 0.5rem;
2791
3407
  margin: 0;
2792
3408
  overflow: visible;
2793
3409
  color: inherit;
2794
3410
  border: 0;
2795
- filter: blur(0px);
2796
- transition-timing-function: var(--media-popup-transition-timing-function);
2797
- transition-duration: var(--media-popup-transition-duration);
2798
- transition-property: scale, opacity, filter;
3411
+ transition: var(--media-popup-transition);
2799
3412
 
2800
3413
  &[data-starting-style],
2801
3414
  &[data-ending-style] {
2802
3415
  opacity: 0;
2803
- filter: blur(8px);
2804
- scale: 0.85;
3416
+ filter: blur(4px);
3417
+ /* We have to use transform here for translate as the translate property is used for positioning by core. */
3418
+ transform: translate(var(--popup-translate-x-distance, 0), var(--popup-translate-y-distance, 0));
3419
+ scale: 0.95;
2805
3420
  }
2806
3421
 
2807
- &[data-instant] {
2808
- transition-duration: 0ms;
3422
+ &[data-ending-style] {
3423
+ transform: none;
3424
+ /* Speed up the exit transition. */
3425
+ transition-duration: max(0ms, calc(var(--media-popup-transition-duration) - 50ms));
2809
3426
  }
2810
3427
 
2811
3428
  &[data-side="top"] {
3429
+ --popup-translate-y-distance: var(--popup-translate-distance);
2812
3430
  transform-origin: bottom;
2813
3431
  }
2814
3432
  &[data-side="bottom"] {
3433
+ --popup-translate-y-distance: calc(var(--popup-translate-distance) * -1);
2815
3434
  transform-origin: top;
2816
3435
  }
2817
3436
  &[data-side="left"] {
3437
+ --popup-translate-x-distance: var(--popup-translate-distance);
2818
3438
  transform-origin: right;
2819
3439
  }
2820
3440
  &[data-side="right"] {
3441
+ --popup-translate-x-distance: calc(var(--popup-translate-distance) * -1);
2821
3442
  transform-origin: left;
2822
3443
  }
2823
3444
 
@@ -2878,6 +3499,13 @@ function VolumePopover(): ReactNode {
2878
3499
  white-space: nowrap;
2879
3500
  border-radius: calc(infinity * 1px);
2880
3501
 
3502
+ /* `display: flex` must not apply while closed — it overrides UA `[popover]` hiding. */
3503
+ &[data-open] {
3504
+ display: flex;
3505
+ column-gap: 0.25rem;
3506
+ align-items: center;
3507
+ }
3508
+
2881
3509
  &[data-side="top"]::before,
2882
3510
  &[data-side="bottom"]::before {
2883
3511
  height: var(--media-tooltip-side-offset);
@@ -2886,6 +3514,250 @@ function VolumePopover(): ReactNode {
2886
3514
  &[data-side="right"]::before {
2887
3515
  width: var(--media-tooltip-side-offset);
2888
3516
  }
3517
+
3518
+ & .media-tooltip__kbd {
3519
+ min-width: 1.5em;
3520
+ padding: 0.1em;
3521
+ font-family: inherit;
3522
+ font-size: 90%;
3523
+ font-weight: 600;
3524
+ line-height: 1.25;
3525
+ text-align: center;
3526
+ background-color: oklch(from currentColor l c h / 0.3);
3527
+ border-radius: 0.25rem;
3528
+ }
3529
+ }
3530
+
3531
+ /* ==========================================================================
3532
+ Menus
3533
+ Note: Menus use `.media-popover` styles for positioning and transitions.
3534
+ ========================================================================== */
3535
+
3536
+ .media-default-skin .media-menu {
3537
+ --menu-transition-duration: 250ms;
3538
+ --menu-max-height: 14rem;
3539
+ --menu-padding: 0.25rem;
3540
+ --menu-border-radius: 0.75rem;
3541
+ --menu-item-border-radius: calc(var(--menu-border-radius) - var(--menu-padding));
3542
+ box-sizing: border-box;
3543
+ max-width: var(--media-popover-available-width, none);
3544
+ max-height: min(var(--media-popover-available-height, var(--menu-max-height)), var(--menu-max-height));
3545
+ padding: var(--menu-padding);
3546
+ overflow: auto;
3547
+ overscroll-behavior: none;
3548
+ border-radius: var(--menu-border-radius);
3549
+
3550
+ @media (prefers-reduced-motion: reduce) {
3551
+ --menu-transition-duration: 0ms;
3552
+ }
3553
+
3554
+ & .media-menu__panel {
3555
+ position: absolute;
3556
+ inset: 0;
3557
+ padding: var(--menu-padding);
3558
+ overflow: auto;
3559
+ overscroll-behavior: none;
3560
+ outline: none;
3561
+ translate: 0 0;
3562
+ transition-timing-function: ease-in-out;
3563
+ transition-duration: var(--menu-transition-duration);
3564
+ transition-property: translate, filter;
3565
+ will-change: translate, filter;
3566
+
3567
+ &[data-starting-style],
3568
+ &[data-ending-style] {
3569
+ overflow: hidden;
3570
+ }
3571
+
3572
+ /* Root settings view — slides out when a submenu is active */
3573
+ &[data-menu-root-view][data-menu-view-state="inactive"] {
3574
+ filter: blur(8px);
3575
+ translate: -100% 0;
3576
+
3577
+ /* Hide the indicator while transitioning */
3578
+ & .media-menu__group::before {
3579
+ display: none;
3580
+ }
3581
+ }
3582
+
3583
+ /* Submenu panels — slide in/out alongside the root view */
3584
+ &[data-submenu] {
3585
+ z-index: 10;
3586
+
3587
+ &:not([data-open], [data-ending-style]) {
3588
+ translate: -100% 0;
3589
+ transition-property: none;
3590
+ }
3591
+
3592
+ &[data-starting-style],
3593
+ &[data-ending-style] {
3594
+ pointer-events: none;
3595
+ filter: blur(8px);
3596
+ }
3597
+
3598
+ &[data-starting-style][data-direction="forward"],
3599
+ &[data-ending-style][data-direction="back"] {
3600
+ translate: 100% 0;
3601
+ }
3602
+
3603
+ &[data-ending-style][data-direction="forward"],
3604
+ &[data-starting-style][data-direction="back"] {
3605
+ translate: -100% 0;
3606
+ }
3607
+ }
3608
+ }
3609
+
3610
+ & .media-menu__separator {
3611
+ margin-block: 0.25rem;
3612
+ border-bottom: 1px solid oklch(0 0 0 / 0.1);
3613
+ box-shadow: 0 1px 0 0 oklch(1 0 0 / 0.075);
3614
+ }
3615
+
3616
+ & .media-menu__group {
3617
+ display: flex;
3618
+ flex-direction: column;
3619
+ gap: 0.125rem;
3620
+
3621
+ @supports (top: anchor(top)) {
3622
+ &::before {
3623
+ position: absolute;
3624
+ position-anchor: --media-menu-item-highlight-anchor;
3625
+ inset: anchor(inside);
3626
+ pointer-events: none;
3627
+ content: "";
3628
+ background-color: oklch(from currentColor l c h / 0.1);
3629
+ border-radius: var(--menu-item-border-radius);
3630
+ transition: inset ease-in-out 100ms;
3631
+ }
3632
+ }
3633
+ }
3634
+
3635
+ & .media-menu__item,
3636
+ & .media-menu__back {
3637
+ display: flex;
3638
+ gap: 0.375rem;
3639
+ align-items: center;
3640
+ padding: 0.375rem 0.5rem;
3641
+ text-align: left;
3642
+ text-shadow: 0 1px 0 var(--media-current-shadow-color);
3643
+ cursor: pointer;
3644
+ user-select: none;
3645
+ outline: 2px solid transparent;
3646
+ outline-offset: -2px;
3647
+ border-radius: var(--menu-item-border-radius);
3648
+ transition: background-color, color;
3649
+ transition-timing-function: ease-out;
3650
+ transition-duration: 100ms;
3651
+
3652
+ &:focus-visible {
3653
+ outline-color: currentColor;
3654
+ outline-offset: 2px;
3655
+ }
3656
+
3657
+ &:hover,
3658
+ &[data-highlighted] {
3659
+ background-color: oklch(from currentColor l c h / 0.1);
3660
+
3661
+ @supports (top: anchor(top)) {
3662
+ anchor-name: --media-menu-item-highlight-anchor;
3663
+ background-color: transparent;
3664
+ }
3665
+ }
3666
+
3667
+ & .media-icon {
3668
+ flex-shrink: 0;
3669
+ color: oklch(from currentColor l c h / 0.65);
3670
+ filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
3671
+ }
3672
+ }
3673
+
3674
+ & .media-menu__indicator {
3675
+ flex-shrink: 0;
3676
+ margin-right: -0.25rem;
3677
+ margin-left: auto;
3678
+ opacity: 0;
3679
+
3680
+ & .media-icon {
3681
+ filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
3682
+ }
3683
+ }
3684
+
3685
+ & .media-menu__item {
3686
+ justify-content: space-between;
3687
+ font-variant-numeric: tabular-nums;
3688
+ color: inherit;
3689
+
3690
+ &[aria-disabled="true"] {
3691
+ pointer-events: none;
3692
+ cursor: not-allowed;
3693
+ opacity: 0.5;
3694
+ }
3695
+
3696
+ &[aria-checked="true"] .media-menu__indicator {
3697
+ opacity: 1;
3698
+ }
3699
+
3700
+ &[data-availability="unavailable"],
3701
+ &[data-availability="unsupported"] {
3702
+ display: none;
3703
+ }
3704
+ }
3705
+
3706
+ & .media-menu__tier {
3707
+ padding-top: 1px;
3708
+ padding-left: 0.125rem;
3709
+ font-size: 0.5625rem;
3710
+ font-weight: 600;
3711
+ line-height: 1;
3712
+ color: oklch(from currentColor l c h / 0.7);
3713
+ }
3714
+
3715
+ & .media-menu__back {
3716
+ width: 100%;
3717
+ margin-bottom: 0.125rem;
3718
+ }
3719
+
3720
+ & .media-menu__hint {
3721
+ display: inline-flex;
3722
+ gap: 0.25rem;
3723
+ align-items: center;
3724
+ min-width: 0;
3725
+ padding-left: 0.5rem;
3726
+ margin-left: auto;
3727
+ font-size: 0.75rem;
3728
+ color: oklch(from currentColor l c h / 0.65);
3729
+ }
3730
+
3731
+ & .media-menu__hint-label {
3732
+ max-width: 6rem;
3733
+ overflow: hidden;
3734
+ text-overflow: ellipsis;
3735
+ white-space: nowrap;
3736
+ }
3737
+
3738
+ & .media-menu__chevron {
3739
+ width: 0.875rem;
3740
+ height: 0.875rem;
3741
+ }
3742
+
3743
+ /* Settings menu */
3744
+ &.media-menu--settings {
3745
+ width: var(--media-menu-width);
3746
+ min-width: 12rem;
3747
+ height: var(--media-menu-height);
3748
+ overflow: hidden;
3749
+ /* Add height and width transitions. */
3750
+ transition:
3751
+ var(--media-popup-transition),
3752
+ height var(--media-popup-transition-timing-function) var(--menu-transition-duration),
3753
+ width var(--media-popup-transition-timing-function) var(--menu-transition-duration);
3754
+
3755
+ /* Don't transition width and height on open/close. */
3756
+ &[data-starting-style],
3757
+ &[data-ending-style] {
3758
+ transition: var(--media-popup-transition);
3759
+ }
3760
+ }
2889
3761
  }
2890
3762
 
2891
3763
  /* ==========================================================================
@@ -2912,10 +3784,11 @@ function VolumePopover(): ReactNode {
2912
3784
 
2913
3785
  /* Play: ended → restart */
2914
3786
  .media-button--play[data-ended] .media-icon--restart,
2915
- /* Play: paused (not ended) → play */
3787
+ /* Play: paused or not yet started (not ended) → play */
2916
3788
  .media-button--play:not([data-ended])[data-paused] .media-icon--play,
2917
- /* Play: playing (not paused, not ended) → pause */
2918
- .media-button--play:not([data-paused]):not([data-ended]) .media-icon--pause,
3789
+ .media-button--play:not([data-ended]):not([data-started]) .media-icon--play,
3790
+ /* Play: started and not paused/ended pause */
3791
+ .media-button--play[data-started]:not([data-paused]):not([data-ended]) .media-icon--pause,
2919
3792
  /* Mute: muted → volume off */
2920
3793
  .media-button--mute[data-muted] .media-icon--volume-off,
2921
3794
  /* Mute: volume low (not muted) → volume low */
@@ -3016,6 +3889,26 @@ function VolumePopover(): ReactNode {
3016
3889
  color: var(--media-text-color);
3017
3890
  }
3018
3891
 
3892
+ /* ==========================================================================
3893
+ Buffering (spinner over play button — overlay only, no layout change)
3894
+ ========================================================================== */
3895
+
3896
+ .media-default-skin--audio .media-button--play__wrapper {
3897
+ position: relative;
3898
+ display: inline-flex;
3899
+ }
3900
+
3901
+ .media-default-skin--audio .media-button--play__wrapper .media-buffering-indicator {
3902
+ color: inherit;
3903
+ }
3904
+
3905
+ .media-default-skin--audio
3906
+ .media-button--play__wrapper:has(.media-buffering-indicator[data-visible])
3907
+ .media-button--play
3908
+ .media-icon {
3909
+ opacity: 0;
3910
+ }
3911
+
3019
3912
  /* ==========================================================================
3020
3913
  Sliders
3021
3914
  ========================================================================== */
@@ -3041,8 +3934,8 @@ skin.css
3041
3934
  'use client';
3042
3935
 
3043
3936
  import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode, isValidElement } from 'react';
3044
- import { CaptionsOffIcon, CaptionsOnIcon, CastEnterIcon, CastExitIcon, CheckIcon, ChevronIcon, FullscreenEnterIcon, FullscreenExitIcon, PauseIcon, PipEnterIcon, PipExitIcon, PlayIcon, RestartIcon, SeekIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons/minimal';
3045
- import { createPlayer, Poster, Container, usePlayer, BufferingIndicator, CaptionsButton, CastButton, Controls, ErrorDialog, FullscreenButton, Gesture, Hotkey, Menu, MuteButton, PiPButton, PlayButton, PlaybackRateMenu, usePlaybackRateMenu, Popover, SeekButton, SeekIndicator, Slider, StatusAnnouncer, StatusIndicator, Time, TimeSlider, Tooltip, VolumeIndicator, VolumeSlider, type RenderProp } from '@videojs/react';
3937
+ import { AirPlayEnterIcon, AirPlayExitIcon, CaptionsOffIcon, CaptionsOnIcon, CastEnterIcon, CastExitIcon, CheckIcon, ChevronIcon, FullscreenEnterIcon, FullscreenExitIcon, GearIcon, PauseIcon, PipEnterIcon, PipExitIcon, PlayIcon, QualityIcon, RestartIcon, SeekIcon, SpeechIcon, SpeedIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons/minimal';
3938
+ import { createPlayer, Poster, Container, usePlayer, AirPlayButton, useAudioTrackOptions, BufferingIndicator, useCaptionsOptions, CastButton, Controls, ErrorDialog, FullscreenButton, Gesture, Hotkey, Menu, MuteButton, PiPButton, PlayButton, usePlaybackRateOptions, Popover, useQualityOptions, SeekButton, SeekIndicator, Slider, StatusAnnouncer, StatusIndicator, Time, TimeSlider, Tooltip, VolumeIndicator, VolumeSlider, type RenderProp } from '@videojs/react';
3046
3939
  import { Video, videoFeatures } from '@videojs/react/video';
3047
3940
  import './player.css';
3048
3941
 
@@ -3050,20 +3943,237 @@ const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictur
3050
3943
 
3051
3944
  const CENTER_STATUS_ACTIONS = ['togglePaused'] as const;
3052
3945
 
3053
- function PlaybackRateMenuItems(): ReactNode {
3054
- const { options, setValue, value } = usePlaybackRateMenu();
3946
+ function MenuChevron({ flipped = false }: { flipped?: boolean }): ReactNode {
3947
+ return <ChevronIcon className={`media-icon media-menu__chevron ${flipped ? 'media-icon--flipped' : undefined}`} />;
3948
+ }
3949
+
3950
+ function SettingsMenu(): ReactNode {
3951
+ const playbackRate = usePlaybackRateOptions();
3952
+ const quality = useQualityOptions();
3953
+ const audioTrack = useAudioTrackOptions();
3954
+ const captions = useCaptionsOptions();
3955
+ const hasPlaybackRate = playbackRate?.state.availability === 'available';
3956
+ const hasQuality = quality?.state.availability === 'available';
3957
+ const hasAudioTrack = audioTrack?.state.availability === 'available';
3958
+ const hasCaptions = captions?.state.availability === 'available';
3959
+
3960
+ if (!hasPlaybackRate && !hasQuality && !hasAudioTrack && !hasCaptions) return null;
3055
3961
 
3056
3962
  return (
3057
- <Menu.RadioGroup className="media-menu__group" value={value} onValueChange={setValue} label="Playback rate">
3058
- {options.map((option) => (
3059
- <Menu.RadioItem key={option.value} className="media-menu__item" value={option.value} disabled={option.disabled}>
3060
- <span>{option.label}</span>
3061
- <Menu.ItemIndicator checked={option.value === value} forceMount className="media-menu__indicator">
3062
- <CheckIcon className="media-icon" />
3063
- </Menu.ItemIndicator>
3064
- </Menu.RadioItem>
3065
- ))}
3066
- </Menu.RadioGroup>
3963
+ <Menu.Root side="top" align="center">
3964
+ <Menu.Trigger aria-label="Settings" className="media-button--settings" render={<Button />}>
3965
+ <GearIcon className="media-icon media-icon--settings" />
3966
+ </Menu.Trigger>
3967
+ <Menu.Content className="media-popover media-menu media-menu--settings">
3968
+ <Menu.View className="media-menu__panel">
3969
+ <div className="media-menu__group">
3970
+ {hasQuality ? (
3971
+ <Menu.Root>
3972
+ <Menu.Trigger
3973
+ type="quality"
3974
+ className="media-menu__item media-menu__item--submenu"
3975
+ render={(props) => (
3976
+ <div {...props}>
3977
+ <QualityIcon className="media-icon" />
3978
+ <span>Quality</span>
3979
+ <span className="media-menu__hint">
3980
+ <Menu.ItemValue className="media-menu__hint-label" />
3981
+ <MenuChevron />
3982
+ </span>
3983
+ </div>
3984
+ )}
3985
+ />
3986
+ <Menu.Content className="media-menu__panel">
3987
+ <Menu.Back className="media-menu__back">
3988
+ <MenuChevron flipped />
3989
+ Quality
3990
+ </Menu.Back>
3991
+ <Menu.Separator className="media-menu__separator" />
3992
+ <Menu.RadioGroup
3993
+ className="media-menu__group"
3994
+ value={quality.value}
3995
+ onValueChange={quality.setValue}
3996
+ aria-label="Quality"
3997
+ >
3998
+ {quality.options.map((option) => (
3999
+ <Menu.RadioItem
4000
+ key={option.value}
4001
+ className="media-menu__item"
4002
+ value={option.value}
4003
+ disabled={option.disabled}
4004
+ >
4005
+ <span>
4006
+ {option.label}
4007
+ {option.tier ? <sup className="media-menu__tier">{option.tier}</sup> : null}
4008
+ </span>
4009
+ {option.badge ? <span className="media-badge">{option.badge}</span> : null}
4010
+ <Menu.ItemIndicator
4011
+ checked={option.value === quality.value}
4012
+ forceMount
4013
+ className="media-menu__indicator"
4014
+ >
4015
+ <CheckIcon className="media-icon" />
4016
+ </Menu.ItemIndicator>
4017
+ </Menu.RadioItem>
4018
+ ))}
4019
+ </Menu.RadioGroup>
4020
+ </Menu.Content>
4021
+ </Menu.Root>
4022
+ ) : null}
4023
+
4024
+ {hasAudioTrack ? (
4025
+ <Menu.Root>
4026
+ <Menu.Trigger
4027
+ type="audio-track"
4028
+ className="media-menu__item media-menu__item--submenu"
4029
+ render={(props) => (
4030
+ <div {...props}>
4031
+ <SpeechIcon className="media-icon" />
4032
+ <span>Audio</span>
4033
+ <span className="media-menu__hint">
4034
+ <Menu.ItemValue className="media-menu__hint-label" />
4035
+ <MenuChevron />
4036
+ </span>
4037
+ </div>
4038
+ )}
4039
+ />
4040
+ <Menu.Content className="media-menu__panel">
4041
+ <Menu.Back className="media-menu__back">
4042
+ <MenuChevron flipped />
4043
+ Audio
4044
+ </Menu.Back>
4045
+ <Menu.Separator className="media-menu__separator" />
4046
+ <Menu.RadioGroup
4047
+ className="media-menu__group"
4048
+ value={audioTrack.value}
4049
+ onValueChange={audioTrack.setValue}
4050
+ aria-label="Audio tracks"
4051
+ >
4052
+ {audioTrack.options.map((option) => (
4053
+ <Menu.RadioItem
4054
+ key={option.value}
4055
+ className="media-menu__item"
4056
+ value={option.value}
4057
+ disabled={option.disabled}
4058
+ >
4059
+ <span>{option.label}</span>
4060
+ <Menu.ItemIndicator
4061
+ checked={option.value === audioTrack.value}
4062
+ forceMount
4063
+ className="media-menu__indicator"
4064
+ >
4065
+ <CheckIcon className="media-icon" />
4066
+ </Menu.ItemIndicator>
4067
+ </Menu.RadioItem>
4068
+ ))}
4069
+ </Menu.RadioGroup>
4070
+ </Menu.Content>
4071
+ </Menu.Root>
4072
+ ) : null}
4073
+
4074
+ {hasPlaybackRate ? (
4075
+ <Menu.Root>
4076
+ <Menu.Trigger
4077
+ type="playback-rate"
4078
+ className="media-menu__item media-menu__item--submenu"
4079
+ render={(props) => (
4080
+ <div {...props}>
4081
+ <SpeedIcon className="media-icon" />
4082
+ <span>Speed</span>
4083
+ <span className="media-menu__hint">
4084
+ <Menu.ItemValue className="media-menu__hint-label" />
4085
+ <MenuChevron />
4086
+ </span>
4087
+ </div>
4088
+ )}
4089
+ />
4090
+ <Menu.Content className="media-menu__panel">
4091
+ <Menu.Back className="media-menu__back">
4092
+ <MenuChevron flipped />
4093
+ Speed
4094
+ </Menu.Back>
4095
+ <Menu.Separator className="media-menu__separator" />
4096
+ <Menu.RadioGroup
4097
+ className="media-menu__group"
4098
+ value={playbackRate.value}
4099
+ onValueChange={playbackRate.setValue}
4100
+ aria-label="Playback rate"
4101
+ >
4102
+ {playbackRate.options.map((option) => (
4103
+ <Menu.RadioItem
4104
+ key={option.value}
4105
+ className="media-menu__item"
4106
+ value={option.value}
4107
+ disabled={option.disabled}
4108
+ >
4109
+ <span>{option.label}</span>
4110
+ <Menu.ItemIndicator
4111
+ checked={option.value === playbackRate.value}
4112
+ forceMount
4113
+ className="media-menu__indicator"
4114
+ >
4115
+ <CheckIcon className="media-icon" />
4116
+ </Menu.ItemIndicator>
4117
+ </Menu.RadioItem>
4118
+ ))}
4119
+ </Menu.RadioGroup>
4120
+ </Menu.Content>
4121
+ </Menu.Root>
4122
+ ) : null}
4123
+
4124
+ {hasCaptions ? (
4125
+ <Menu.Root>
4126
+ <Menu.Trigger
4127
+ type="captions"
4128
+ className="media-menu__item media-menu__item--submenu"
4129
+ render={(props) => (
4130
+ <div {...props}>
4131
+ <CaptionsOffIcon className="media-icon" />
4132
+ <span>Captions</span>
4133
+ <span className="media-menu__hint">
4134
+ <Menu.ItemValue className="media-menu__hint-label" />
4135
+ <MenuChevron />
4136
+ </span>
4137
+ </div>
4138
+ )}
4139
+ />
4140
+ <Menu.Content className="media-menu__panel">
4141
+ <Menu.Back className="media-menu__back">
4142
+ <MenuChevron flipped />
4143
+ Captions
4144
+ </Menu.Back>
4145
+ <Menu.Separator className="media-menu__separator" />
4146
+ <Menu.RadioGroup
4147
+ className="media-menu__group"
4148
+ value={captions.value}
4149
+ onValueChange={captions.setValue}
4150
+ aria-label="Captions"
4151
+ >
4152
+ {captions.options.map((option) => (
4153
+ <Menu.RadioItem
4154
+ key={option.value}
4155
+ className="media-menu__item"
4156
+ value={option.value}
4157
+ disabled={option.disabled}
4158
+ >
4159
+ <span>{option.label}</span>
4160
+ <Menu.ItemIndicator
4161
+ checked={option.value === captions.value}
4162
+ forceMount
4163
+ className="media-menu__indicator"
4164
+ >
4165
+ <CheckIcon className="media-icon" />
4166
+ </Menu.ItemIndicator>
4167
+ </Menu.RadioItem>
4168
+ ))}
4169
+ </Menu.RadioGroup>
4170
+ </Menu.Content>
4171
+ </Menu.Root>
4172
+ ) : null}
4173
+ </div>
4174
+ </Menu.View>
4175
+ </Menu.Content>
4176
+ </Menu.Root>
3067
4177
  );
3068
4178
  }
3069
4179
 
@@ -3091,10 +4201,18 @@ export interface VideoPlayerProps {
3091
4201
  * />
3092
4202
  * ```
3093
4203
  */
3094
- export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProps): ReactNode {
4204
+ export function VideoPlayer({ src, className, poster, placeholder, style, ...rest }: VideoPlayerProps): ReactNode {
4205
+ const containerStyle = placeholder
4206
+ ? ({ '--media-poster-placeholder': `url(${placeholder})`, ...style } as CSSProperties)
4207
+ : style;
4208
+
3095
4209
  return (
3096
4210
  <Player.Provider>
3097
- <Container className={`media-minimal-skin media-minimal-skin--video ${className ?? ''}`} {...rest}>
4211
+ <Container
4212
+ className={`media-minimal-skin media-minimal-skin--video ${className ?? ''}`}
4213
+ style={containerStyle}
4214
+ {...rest}
4215
+ >
3098
4216
  <Video src={src} playsInline />
3099
4217
 
3100
4218
  {poster && (
@@ -3136,7 +4254,10 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
3136
4254
  </PlayButton>
3137
4255
  }
3138
4256
  />
3139
- <Tooltip.Popup className="media-tooltip" />
4257
+ <Tooltip.Popup className="media-tooltip">
4258
+ <Tooltip.Label />
4259
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
4260
+ </Tooltip.Popup>
3140
4261
  </Tooltip.Root>
3141
4262
 
3142
4263
  <Tooltip.Root side="top">
@@ -3150,7 +4271,10 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
3150
4271
  </SeekButton>
3151
4272
  }
3152
4273
  />
3153
- <Tooltip.Popup className="media-tooltip" />
4274
+ <Tooltip.Popup className="media-tooltip">
4275
+ <Tooltip.Label />
4276
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
4277
+ </Tooltip.Popup>
3154
4278
  </Tooltip.Root>
3155
4279
 
3156
4280
  <Tooltip.Root side="top">
@@ -3164,13 +4288,16 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
3164
4288
  </SeekButton>
3165
4289
  }
3166
4290
  />
3167
- <Tooltip.Popup className="media-tooltip" />
4291
+ <Tooltip.Popup className="media-tooltip">
4292
+ <Tooltip.Label />
4293
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
4294
+ </Tooltip.Popup>
3168
4295
  </Tooltip.Root>
3169
4296
  </div>
3170
4297
 
3171
4298
  <div className="media-time-controls">
3172
4299
  <Time.Group className="media-time-group">
3173
- <Time.Value type="current" className="media-time media-time--current" />
4300
+ <Time.Value toggle type="current" className="media-time media-time--current" />
3174
4301
  <Time.Separator className="media-time-separator" />
3175
4302
  <Time.Value type="duration" className="media-time media-time--duration" />
3176
4303
  </Time.Group>
@@ -3182,48 +4309,52 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
3182
4309
  </TimeSlider.Track>
3183
4310
  <TimeSlider.Thumb className="media-slider__thumb" />
3184
4311
 
3185
- <div className="media-preview media-slider__preview">
3186
- <div className="media-preview__thumbnail-wrapper">
3187
- <Slider.Thumbnail className="media-preview__thumbnail" />
4312
+ <div className="media-thumbnail media-slider__thumbnail">
4313
+ <div className="media-thumbnail__image-wrapper">
4314
+ <Slider.Thumbnail className="media-thumbnail__image" />
3188
4315
  </div>
3189
- <TimeSlider.Value type="pointer" className="media-time media-preview__time" />
3190
- <SpinnerIcon className="media-preview__spinner media-icon" />
4316
+ <TimeSlider.Value type="pointer" className="media-time media-thumbnail__time" />
4317
+ <SpinnerIcon className="media-thumbnail__spinner media-icon" />
3191
4318
  </div>
4319
+ <TimeSlider.Preview className="media-slider__preview">
4320
+ <TimeSlider.Value type="pointer" className="media-time media-slider__value" />
4321
+ </TimeSlider.Preview>
3192
4322
  </TimeSlider.Root>
3193
4323
  </div>
3194
4324
 
3195
4325
  <div className="media-button-group">
3196
- <PlaybackRateMenu.Root side="top" align="center">
3197
- <PlaybackRateMenu.Trigger className="media-button--playback-rate" render={<Button />} />
3198
- <PlaybackRateMenu.Content className="media-popover media-menu media-menu--playback-rate">
3199
- <PlaybackRateMenuItems />
3200
- </PlaybackRateMenu.Content>
3201
- </PlaybackRateMenu.Root>
3202
-
3203
4326
  <VolumePopover />
3204
4327
 
4328
+ <SettingsMenu />
4329
+
3205
4330
  <Tooltip.Root side="top">
3206
4331
  <Tooltip.Trigger
3207
4332
  render={
3208
- <CaptionsButton className="media-button--captions" render={<Button />}>
3209
- <CaptionsOffIcon className="media-icon media-icon--captions-off" />
3210
- <CaptionsOnIcon className="media-icon media-icon--captions-on" />
3211
- </CaptionsButton>
4333
+ <CastButton className="media-button--cast" render={<Button />}>
4334
+ <CastEnterIcon className="media-icon media-icon--cast-enter" />
4335
+ <CastExitIcon className="media-icon media-icon--cast-exit" />
4336
+ </CastButton>
3212
4337
  }
3213
4338
  />
3214
- <Tooltip.Popup className="media-tooltip" />
4339
+ <Tooltip.Popup className="media-tooltip">
4340
+ <Tooltip.Label />
4341
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
4342
+ </Tooltip.Popup>
3215
4343
  </Tooltip.Root>
3216
4344
 
3217
4345
  <Tooltip.Root side="top">
3218
4346
  <Tooltip.Trigger
3219
4347
  render={
3220
- <CastButton className="media-button--cast" render={<Button />}>
3221
- <CastEnterIcon className="media-icon media-icon--cast-enter" />
3222
- <CastExitIcon className="media-icon media-icon--cast-exit" />
3223
- </CastButton>
4348
+ <AirPlayButton className="media-button--airplay" render={<Button />}>
4349
+ <AirPlayEnterIcon className="media-icon media-icon--airplay-enter" />
4350
+ <AirPlayExitIcon className="media-icon media-icon--airplay-exit" />
4351
+ </AirPlayButton>
3224
4352
  }
3225
4353
  />
3226
- <Tooltip.Popup className="media-tooltip" />
4354
+ <Tooltip.Popup className="media-tooltip">
4355
+ <Tooltip.Label />
4356
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
4357
+ </Tooltip.Popup>
3227
4358
  </Tooltip.Root>
3228
4359
 
3229
4360
  <Tooltip.Root side="top">
@@ -3235,7 +4366,10 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
3235
4366
  </PiPButton>
3236
4367
  }
3237
4368
  />
3238
- <Tooltip.Popup className="media-tooltip" />
4369
+ <Tooltip.Popup className="media-tooltip">
4370
+ <Tooltip.Label />
4371
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
4372
+ </Tooltip.Popup>
3239
4373
  </Tooltip.Root>
3240
4374
 
3241
4375
  <Tooltip.Root side="top">
@@ -3247,7 +4381,10 @@ export function VideoPlayer({ src, className, poster, ...rest }: VideoPlayerProp
3247
4381
  </FullscreenButton>
3248
4382
  }
3249
4383
  />
3250
- <Tooltip.Popup className="media-tooltip" />
4384
+ <Tooltip.Popup className="media-tooltip">
4385
+ <Tooltip.Label />
4386
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
4387
+ </Tooltip.Popup>
3251
4388
  </Tooltip.Root>
3252
4389
  </div>
3253
4390
  </Tooltip.Provider>
@@ -3417,6 +4554,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3417
4554
  --media-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.15));
3418
4555
  --media-current-shadow-color-subtle: oklch(from var(--media-current-shadow-color) l c h / calc(alpha * 0.4));
3419
4556
  --media-icon-size: 18px;
4557
+ --media-color-scrollbar-thumb: oklch(from currentColor l c h / 0.3);
3420
4558
  position: relative;
3421
4559
  display: block;
3422
4560
  width: 100%;
@@ -3435,6 +4573,8 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3435
4573
  letter-spacing: normal;
3436
4574
  outline: 2px solid transparent;
3437
4575
  outline-offset: -4px;
4576
+ scrollbar-color: var(--media-color-scrollbar-thumb) transparent;
4577
+ scrollbar-width: thin;
3438
4578
  border-radius: var(--media-border-radius, 0.75rem);
3439
4579
  isolation: isolate;
3440
4580
  transition-timing-function: ease-out;
@@ -3445,6 +4585,16 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3445
4585
  outline-color: currentColor;
3446
4586
  outline-offset: 2px;
3447
4587
  }
4588
+
4589
+ &::-webkit-scrollbar-thumb {
4590
+ background: var(--media-color-scrollbar-thumb);
4591
+ border-radius: 9999px;
4592
+ }
4593
+
4594
+ @media (prefers-reduced-transparency: reduce) or (prefers-contrast: more) {
4595
+ --media-color-scrollbar-thumb: oklch(from currentColor l c h / 0.8);
4596
+ scrollbar-width: auto;
4597
+ }
3448
4598
  }
3449
4599
 
3450
4600
  /* ==========================================================================
@@ -3498,6 +4648,12 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3498
4648
  opacity: 1;
3499
4649
  }
3500
4650
 
4651
+ .media-minimal-skin .media-buffering-indicator[data-visible] ~ .media-overlay {
4652
+ background: oklch(0 0 0 / 0.35);
4653
+ opacity: 1;
4654
+ backdrop-filter: blur(8px);
4655
+ }
4656
+
3501
4657
  .media-minimal-skin .media-error[data-open] ~ .media-overlay {
3502
4658
  backdrop-filter: blur(16px) saturate(1.2);
3503
4659
  }
@@ -3509,9 +4665,9 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3509
4665
  .media-minimal-skin .media-buffering-indicator {
3510
4666
  position: absolute;
3511
4667
  inset: 0;
4668
+ z-index: 10;
3512
4669
  display: none;
3513
- align-items: center;
3514
- justify-content: center;
4670
+ place-content: center;
3515
4671
  color: oklch(1 0 0);
3516
4672
  pointer-events: none;
3517
4673
 
@@ -3520,7 +4676,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3520
4676
  }
3521
4677
 
3522
4678
  &[data-visible] {
3523
- display: flex;
4679
+ display: grid;
3524
4680
  }
3525
4681
  }
3526
4682
 
@@ -3587,8 +4743,28 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3587
4743
  align-items: center;
3588
4744
  }
3589
4745
 
3590
- .media-minimal-skin .media-time {
3591
- font-variant-numeric: tabular-nums;
4746
+ .media-minimal-skin .media-time {
4747
+ font-variant-numeric: tabular-nums;
4748
+ }
4749
+
4750
+ .media-minimal-skin .media-time[role="button"] {
4751
+ cursor: pointer;
4752
+ outline: 2px solid transparent;
4753
+ outline-offset: -2px;
4754
+ border-radius: 0.25rem;
4755
+ transition-timing-function: ease-out;
4756
+ transition-duration: 100ms;
4757
+ transition-property: outline-color, outline-offset;
4758
+
4759
+ @supports (corner-shape: squircle) {
4760
+ border-radius: 1rem;
4761
+ corner-shape: squircle;
4762
+ }
4763
+
4764
+ &:focus-visible {
4765
+ outline-color: currentColor;
4766
+ outline-offset: 2px;
4767
+ }
3592
4768
  }
3593
4769
 
3594
4770
  .media-minimal-skin .media-time--current,
@@ -3698,6 +4874,10 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3698
4874
  scale: 0.9;
3699
4875
  }
3700
4876
 
4877
+ & .media-icon__container {
4878
+ display: grid;
4879
+ }
4880
+
3701
4881
  & .media-icon {
3702
4882
  grid-area: 1 / 1;
3703
4883
  transition-behavior: allow-discrete;
@@ -3715,8 +4895,9 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3715
4895
  right: -1px;
3716
4896
  bottom: -3px;
3717
4897
  font-size: 10px; /* Hard coded due to size limitations. */
3718
- font-weight: 480;
4898
+ font-weight: 500;
3719
4899
  font-variant-numeric: tabular-nums;
4900
+ letter-spacing: -0.05em;
3720
4901
  }
3721
4902
 
3722
4903
  &:has(.media-icon--flipped) .media-icon__label {
@@ -3740,6 +4921,27 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3740
4921
  }
3741
4922
  }
3742
4923
 
4924
+ /* Settings button */
4925
+ .media-minimal-skin .media-button--settings {
4926
+ display: none;
4927
+
4928
+ & .media-icon--settings {
4929
+ transition: transform 150ms ease-in-out;
4930
+
4931
+ @media (prefers-reduced-motion: reduce) {
4932
+ transition-duration: 0ms;
4933
+ }
4934
+ }
4935
+
4936
+ &[aria-expanded="true"] .media-icon--settings {
4937
+ transform: rotate(90deg);
4938
+ }
4939
+ }
4940
+
4941
+ .media-minimal-skin .media-button-group:has([data-availability="available"]) .media-button--settings {
4942
+ display: grid;
4943
+ }
4944
+
3743
4945
  /* Live button — wide pill button with a status dot (gray → red at the live
3744
4946
  edge) rendered via ::before, and "LIVE" text rendered as the button's own
3745
4947
  text content. */
@@ -3786,6 +4988,21 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3786
4988
  }
3787
4989
  }
3788
4990
 
4991
+ /* ==========================================================================
4992
+ Badge
4993
+ ========================================================================== */
4994
+
4995
+ .media-minimal-skin .media-badge {
4996
+ padding: 0.25rem 0.375rem;
4997
+ font-size: 0.6875rem;
4998
+ font-weight: 500;
4999
+ line-height: 1;
5000
+ color: oklch(from currentColor l c h / 0.85);
5001
+ white-space: nowrap;
5002
+ background-color: oklch(from currentColor l c h / 0.1);
5003
+ border-radius: 0.25rem;
5004
+ }
5005
+
3789
5006
  /* ==========================================================================
3790
5007
  Icons
3791
5008
  ========================================================================== */
@@ -3793,6 +5010,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3793
5010
  .media-minimal-skin .media-icon__container {
3794
5011
  position: relative;
3795
5012
  }
5013
+
3796
5014
  .media-minimal-skin .media-icon {
3797
5015
  flex-shrink: 0;
3798
5016
  width: var(--media-icon-size);
@@ -3816,7 +5034,8 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3816
5034
  transition: opacity 0.25s;
3817
5035
  }
3818
5036
  .media-minimal-skin media-poster:not([data-visible]),
3819
- .media-minimal-skin > img:not([data-visible]) {
5037
+ .media-minimal-skin > img:not([data-visible]),
5038
+ .media-minimal-skin > img[data-visible]:not([data-loaded]) {
3820
5039
  opacity: 0;
3821
5040
  }
3822
5041
  .media-minimal-skin media-poster ::slotted(img),
@@ -3835,6 +5054,30 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3835
5054
  border-radius: inherit;
3836
5055
  }
3837
5056
 
5057
+ /* Blurred placeholder: HTML path targets media-poster::before, React path targets the skin container::before */
5058
+ .media-minimal-skin media-poster::before,
5059
+ .media-minimal-skin::before {
5060
+ position: absolute;
5061
+ inset: 0;
5062
+ pointer-events: none;
5063
+ content: "";
5064
+ background-image: var(--media-poster-placeholder, none);
5065
+ background-repeat: no-repeat;
5066
+ background-position: var(--media-object-position, center);
5067
+ background-size: var(--media-object-fit, contain);
5068
+ filter: blur(var(--media-poster-placeholder-blur, 20px));
5069
+ }
5070
+
5071
+ /* React path: hidden by default, transitions in while poster loads */
5072
+ .media-minimal-skin::before {
5073
+ opacity: 0;
5074
+ transition: opacity 0.25s;
5075
+ }
5076
+ /* Show placeholder while the poster is visible but not yet loaded */
5077
+ .media-minimal-skin:has(img[data-visible]:not([data-loaded]))::before {
5078
+ opacity: 1;
5079
+ }
5080
+
3838
5081
  .media-minimal-skin:fullscreen media-poster ::slotted(img),
3839
5082
  .media-minimal-skin:fullscreen media-poster img,
3840
5083
  .media-minimal-skin:fullscreen > img {
@@ -3842,22 +5085,23 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3842
5085
  }
3843
5086
 
3844
5087
  /* ==========================================================================
3845
- Media preview
5088
+ Media thumbnail
3846
5089
  ========================================================================== */
3847
- .media-minimal-skin .media-preview {
5090
+
5091
+ .media-minimal-skin .media-thumbnail {
3848
5092
  pointer-events: none;
3849
5093
 
3850
- & .media-preview__thumbnail-wrapper {
5094
+ & .media-thumbnail__image-wrapper {
3851
5095
  position: relative;
3852
5096
  background-color: oklch(0 0 0 / 0.9);
3853
5097
  border-radius: 0.5rem;
3854
5098
  }
3855
- & .media-preview__thumbnail {
5099
+ & .media-thumbnail__image {
3856
5100
  display: block;
3857
5101
  border-radius: inherit;
3858
5102
  }
3859
5103
 
3860
- & .media-preview__time {
5104
+ & .media-thumbnail__time {
3861
5105
  display: block;
3862
5106
  margin-top: 0.5rem;
3863
5107
  text-align: center;
@@ -3867,7 +5111,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3867
5111
  opacity: 1;
3868
5112
  }
3869
5113
 
3870
- & .media-preview__spinner {
5114
+ & .media-thumbnail__spinner {
3871
5115
  position: absolute;
3872
5116
  top: 50%;
3873
5117
  left: 50%;
@@ -3875,22 +5119,22 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3875
5119
  translate: -50% -50%;
3876
5120
  }
3877
5121
 
3878
- & .media-preview__thumbnail,
3879
- & .media-preview__spinner {
5122
+ & .media-thumbnail__image,
5123
+ & .media-thumbnail__spinner {
3880
5124
  transition: opacity 150ms ease-out;
3881
5125
  }
3882
5126
 
3883
- &:not(:has(.media-preview__thumbnail[data-loading])) {
3884
- & .media-preview__spinner {
5127
+ &:not(:has(.media-thumbnail__image[data-loading])) {
5128
+ & .media-thumbnail__spinner {
3885
5129
  --media-spinner-animation: none;
3886
5130
  }
3887
5131
  }
3888
5132
 
3889
- &:has(.media-preview__thumbnail[data-loading]) {
3890
- & .media-preview__thumbnail {
5133
+ &:has(.media-thumbnail__image[data-loading]) {
5134
+ & .media-thumbnail__image {
3891
5135
  opacity: 0;
3892
5136
  }
3893
- & .media-preview__spinner {
5137
+ & .media-thumbnail__spinner {
3894
5138
  opacity: 1;
3895
5139
  }
3896
5140
  }
@@ -3954,7 +5198,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3954
5198
  background-color: currentColor;
3955
5199
  border-radius: calc(infinity * 1px);
3956
5200
  box-shadow:
3957
- 0 0 0 1px var(--media-current-shadow-color-subtle, oklch(0 0 0 / 0.1)),
5201
+ 0 0 0 1px var(--media-current-shadow-color, oklch(0 0 0 / 0.15)),
3958
5202
  0 1px 3px 0 oklch(0 0 0 / 0.15),
3959
5203
  0 1px 2px -1px oklch(0 0 0 / 0.15);
3960
5204
  opacity: 0;
@@ -3988,6 +5232,53 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
3988
5232
  scale: 1;
3989
5233
  }
3990
5234
 
5235
+ /* Preview */
5236
+ .media-minimal-skin .media-slider__preview {
5237
+ & .media-slider__value,
5238
+ &::before {
5239
+ opacity: 0;
5240
+ scale: 0.5;
5241
+ transition-timing-function: ease-out;
5242
+ transition-duration: 200ms;
5243
+ transition-property: opacity, scale;
5244
+ }
5245
+
5246
+ & .media-slider__value {
5247
+ position: absolute;
5248
+ bottom: 1.5rem;
5249
+ text-shadow: 0 1px 0 var(--media-current-shadow-color);
5250
+ filter: blur(8px);
5251
+ translate: -50% 0.5rem;
5252
+ transition-property: filter, opacity, scale, translate;
5253
+ }
5254
+
5255
+ &::before {
5256
+ display: block;
5257
+ content: "";
5258
+ background-color: oklch(from currentColor l c h / 0.35);
5259
+ }
5260
+
5261
+ &[data-pointing] .media-slider__value,
5262
+ &[data-pointing]:not([data-dragging])::before {
5263
+ opacity: 1;
5264
+ scale: 1;
5265
+ }
5266
+ &[data-pointing] .media-slider__value {
5267
+ filter: blur(0);
5268
+ translate: -50% 0;
5269
+ }
5270
+
5271
+ &[data-orientation="horizontal"]::before {
5272
+ min-width: 1px;
5273
+ height: 1.25rem;
5274
+ }
5275
+
5276
+ &[data-orientation="vertical"]::before {
5277
+ width: 1.25rem;
5278
+ min-height: 1px;
5279
+ }
5280
+ }
5281
+
3991
5282
  /* Shared track fills */
3992
5283
  .media-minimal-skin .media-slider__buffer,
3993
5284
  .media-minimal-skin .media-slider__fill {
@@ -4059,37 +5350,52 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4059
5350
  Popups & Animations
4060
5351
  ========================================================================== */
4061
5352
 
5353
+ .media-minimal-skin {
5354
+ --media-popup-transition:
5355
+ opacity var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
5356
+ filter var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
5357
+ transform var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
5358
+ scale var(--media-popup-transition-timing-function) var(--media-popup-transition-duration);
5359
+ }
5360
+
4062
5361
  .media-minimal-skin .media-popover,
4063
5362
  .media-minimal-skin .media-tooltip {
5363
+ --popup-translate-distance: 0.5rem;
4064
5364
  margin: 0;
4065
5365
  overflow: visible;
4066
5366
  color: inherit;
4067
5367
  border: 0;
4068
- transition-timing-function: var(--media-popup-transition-timing-function);
4069
- transition-duration: var(--media-popup-transition-duration);
4070
- transition-property: scale, opacity, filter;
5368
+ transition: var(--media-popup-transition);
4071
5369
 
4072
5370
  &[data-starting-style],
4073
5371
  &[data-ending-style] {
4074
5372
  opacity: 0;
4075
- filter: blur(8px);
4076
- scale: 0.5;
5373
+ filter: blur(4px);
5374
+ /* We have to use transform here for translate as the translate property is used for positioning by core. */
5375
+ transform: translate(var(--popup-translate-x-distance, 0), var(--popup-translate-y-distance, 0));
5376
+ scale: 0.95;
4077
5377
  }
4078
5378
 
4079
- &[data-instant] {
4080
- transition-duration: 0ms;
5379
+ &[data-ending-style] {
5380
+ transform: none;
5381
+ /* Speed up the exit transition. */
5382
+ transition-duration: max(0ms, calc(var(--media-popup-transition-duration) - 50ms));
4081
5383
  }
4082
5384
 
4083
5385
  &[data-side="top"] {
5386
+ --popup-translate-y-distance: var(--popup-translate-distance);
4084
5387
  transform-origin: bottom;
4085
5388
  }
4086
5389
  &[data-side="bottom"] {
5390
+ --popup-translate-y-distance: calc(var(--popup-translate-distance) * -1);
4087
5391
  transform-origin: top;
4088
5392
  }
4089
5393
  &[data-side="left"] {
5394
+ --popup-translate-x-distance: var(--popup-translate-distance);
4090
5395
  transform-origin: right;
4091
5396
  }
4092
5397
  &[data-side="right"] {
5398
+ --popup-translate-x-distance: calc(var(--popup-translate-distance) * -1);
4093
5399
  transform-origin: left;
4094
5400
  }
4095
5401
 
@@ -4145,10 +5451,17 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4145
5451
  border-radius: 0.5rem;
4146
5452
  box-shadow:
4147
5453
  0 0 0 1px var(--media-tooltip-border-color),
4148
- 0 4px 6px -1px oklch(0 0 0 / 0.1),
4149
- 0 2px 4px -2px oklch(0 0 0 / 0.1);
5454
+ 0 4px 6px -1px oklch(0 0 0 / 0.2),
5455
+ 0 2px 4px -2px oklch(0 0 0 / 0.2);
4150
5456
  backdrop-filter: var(--media-tooltip-backdrop-filter);
4151
5457
 
5458
+ /* `display: flex` must not apply while closed — it overrides UA `[popover]` hiding. */
5459
+ &[data-open] {
5460
+ display: flex;
5461
+ column-gap: 0.25rem;
5462
+ align-items: center;
5463
+ }
5464
+
4152
5465
  &[data-side="top"]::before,
4153
5466
  &[data-side="bottom"]::before {
4154
5467
  height: var(--media-tooltip-side-offset);
@@ -4157,6 +5470,19 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4157
5470
  &[data-side="right"]::before {
4158
5471
  width: var(--media-tooltip-side-offset);
4159
5472
  }
5473
+
5474
+ & .media-tooltip__kbd {
5475
+ min-width: 1.5em;
5476
+ padding: 0.1em;
5477
+ margin-right: -0.25rem;
5478
+ font-family: inherit;
5479
+ font-size: 90%;
5480
+ font-weight: 600;
5481
+ line-height: 1.25;
5482
+ text-align: center;
5483
+ background-color: oklch(from currentColor l c h / 0.15);
5484
+ border-radius: 0.25rem;
5485
+ }
4160
5486
  }
4161
5487
 
4162
5488
  .media-minimal-skin .media-popover--volume:has(media-volume-slider[data-availability="unsupported"]) {
@@ -4168,72 +5494,232 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4168
5494
  Note: Menus use `.media-popover` styles for positioning and transitions.
4169
5495
  ========================================================================== */
4170
5496
 
4171
- .media-minimal-skin .media-popover.media-menu {
5497
+ .media-minimal-skin .media-menu {
5498
+ --menu-transition-duration: 250ms;
5499
+ --menu-max-height: 14rem;
5500
+ --menu-padding: 0.5rem;
5501
+ --menu-border-radius: 1rem;
5502
+ --menu-item-border-radius: calc(var(--menu-border-radius) - var(--menu-padding));
4172
5503
  box-sizing: border-box;
4173
- min-width: min(6rem, var(--media-popover-available-width, 6rem));
4174
5504
  max-width: var(--media-popover-available-width, none);
4175
- max-height: var(--media-popover-available-height, none);
4176
- padding: 0.25rem;
5505
+ max-height: min(var(--media-popover-available-height, var(--menu-max-height)), var(--menu-max-height));
5506
+ padding: var(--menu-padding);
4177
5507
  overflow: auto;
4178
5508
  overscroll-behavior: none;
4179
- background-color: var(--media-tooltip-background-color);
4180
- border-radius: 0.75rem;
5509
+ background-color: var(--media-popover-background-color);
5510
+ border-radius: var(--menu-border-radius);
4181
5511
  box-shadow:
4182
- 0 0 0 1px var(--media-tooltip-border-color),
5512
+ 0 0 0 1px var(--media-popover-border-color),
4183
5513
  0 4px 6px -1px oklch(0 0 0 / 0.1),
4184
5514
  0 2px 4px -2px oklch(0 0 0 / 0.1);
4185
- backdrop-filter: var(--media-tooltip-backdrop-filter);
5515
+ backdrop-filter: var(--media-popover-backdrop-filter);
4186
5516
 
4187
- &::before {
4188
- display: none;
5517
+ @media (prefers-reduced-motion: reduce) {
5518
+ --menu-transition-duration: 0ms;
4189
5519
  }
4190
- }
4191
5520
 
4192
- .media-minimal-skin .media-popover.media-menu .media-menu__group {
4193
- display: flex;
4194
- flex-direction: column;
4195
- gap: 0.125rem;
4196
- }
5521
+ & .media-menu__panel {
5522
+ position: absolute;
5523
+ inset: 0;
5524
+ padding: var(--menu-padding);
5525
+ overflow: auto;
5526
+ overscroll-behavior: none;
5527
+ outline: none;
5528
+ translate: 0 0;
5529
+ transition-timing-function: ease-in-out;
5530
+ transition-duration: var(--menu-transition-duration);
5531
+ transition-property: translate, filter;
5532
+ will-change: translate, filter;
5533
+
5534
+ &[data-starting-style],
5535
+ &[data-ending-style] {
5536
+ overflow: hidden;
5537
+ }
4197
5538
 
4198
- .media-minimal-skin .media-popover.media-menu .media-menu__item {
4199
- display: flex;
4200
- gap: 0.5rem;
4201
- align-items: center;
4202
- justify-content: space-between;
4203
- min-height: 2rem;
4204
- padding: 0 0.625rem;
4205
- font-variant-numeric: tabular-nums;
4206
- color: inherit;
4207
- cursor: pointer;
4208
- outline: 2px solid transparent;
4209
- outline-offset: -2px;
4210
- border-radius: 0.5rem;
5539
+ /* Root settings view — slides out when a submenu is active */
5540
+ &[data-menu-root-view][data-menu-view-state="inactive"] {
5541
+ filter: blur(8px);
5542
+ translate: -100% 0;
4211
5543
 
4212
- &:hover,
4213
- &[data-highlighted] {
4214
- background-color: oklch(from currentColor l c h / 0.1);
5544
+ /* Hide the indicator while transitioning */
5545
+ & .media-menu__group::before {
5546
+ display: none;
5547
+ }
5548
+ }
5549
+
5550
+ /* Submenu panels — slide in/out alongside the root view */
5551
+ &[data-submenu] {
5552
+ z-index: 10;
5553
+
5554
+ &:not([data-open], [data-ending-style]) {
5555
+ translate: -100% 0;
5556
+ transition-property: none;
5557
+ }
5558
+
5559
+ &[data-starting-style],
5560
+ &[data-ending-style] {
5561
+ pointer-events: none;
5562
+ filter: blur(8px);
5563
+ }
5564
+
5565
+ &[data-starting-style][data-direction="forward"],
5566
+ &[data-ending-style][data-direction="back"] {
5567
+ translate: 100% 0;
5568
+ }
5569
+
5570
+ &[data-ending-style][data-direction="forward"],
5571
+ &[data-starting-style][data-direction="back"] {
5572
+ translate: -100% 0;
5573
+ }
5574
+ }
4215
5575
  }
4216
5576
 
4217
- &:focus-visible {
4218
- outline-color: currentColor;
4219
- outline-offset: 2px;
5577
+ & .media-menu__separator {
5578
+ margin: 0.25rem 0;
5579
+ border-bottom: 1px solid oklch(1 0 0 / 0.1);
4220
5580
  }
4221
5581
 
4222
- &[aria-disabled="true"] {
4223
- pointer-events: none;
4224
- cursor: not-allowed;
4225
- opacity: 0.5;
5582
+ & .media-menu__group {
5583
+ display: flex;
5584
+ flex-direction: column;
5585
+ gap: 0.125rem;
5586
+
5587
+ @supports (top: anchor(top)) {
5588
+ &::before {
5589
+ position: absolute;
5590
+ position-anchor: --media-menu-item-highlight-anchor;
5591
+ inset: anchor(inside);
5592
+ pointer-events: none;
5593
+ content: "";
5594
+ background-color: oklch(from currentColor l c h / 0.1);
5595
+ border-radius: var(--menu-item-border-radius);
5596
+ transition: inset ease-in-out 100ms;
5597
+ }
5598
+ }
4226
5599
  }
4227
- }
4228
5600
 
4229
- .media-minimal-skin .media-popover.media-menu .media-menu__indicator {
4230
- flex-shrink: 0;
4231
- margin-right: -0.25rem;
4232
- opacity: 0;
4233
- }
5601
+ & .media-menu__item,
5602
+ & .media-menu__back {
5603
+ display: flex;
5604
+ gap: 0.375rem;
5605
+ align-items: center;
5606
+ padding: 0.375rem 0.5rem;
5607
+ text-align: left;
5608
+ text-shadow: 0 1px 0 var(--media-current-shadow-color);
5609
+ cursor: pointer;
5610
+ user-select: none;
5611
+ outline: 2px solid transparent;
5612
+ outline-offset: -2px;
5613
+ border-radius: var(--menu-item-border-radius);
5614
+ transition: background-color, color;
5615
+ transition-timing-function: ease-out;
5616
+ transition-duration: 100ms;
4234
5617
 
4235
- .media-minimal-skin .media-popover.media-menu .media-menu__item[aria-checked="true"] .media-menu__indicator {
4236
- opacity: 1;
5618
+ &:focus-visible {
5619
+ outline-color: currentColor;
5620
+ outline-offset: 2px;
5621
+ }
5622
+
5623
+ &:hover,
5624
+ &[data-highlighted] {
5625
+ background-color: oklch(from currentColor l c h / 0.1);
5626
+
5627
+ @supports (top: anchor(top)) {
5628
+ anchor-name: --media-menu-item-highlight-anchor;
5629
+ background-color: transparent;
5630
+ }
5631
+ }
5632
+
5633
+ & .media-icon {
5634
+ flex-shrink: 0;
5635
+ color: oklch(from currentColor l c h / 0.5);
5636
+ filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
5637
+ }
5638
+ }
5639
+
5640
+ & .media-menu__indicator {
5641
+ flex-shrink: 0;
5642
+ margin-right: -0.25rem;
5643
+ margin-left: auto;
5644
+ opacity: 0;
5645
+ }
5646
+
5647
+ & .media-menu__item {
5648
+ justify-content: space-between;
5649
+ font-variant-numeric: tabular-nums;
5650
+ color: inherit;
5651
+
5652
+ &[aria-disabled="true"] {
5653
+ pointer-events: none;
5654
+ cursor: not-allowed;
5655
+ opacity: 0.5;
5656
+ }
5657
+
5658
+ &[aria-checked="true"] .media-menu__indicator {
5659
+ opacity: 1;
5660
+ }
5661
+
5662
+ &[data-availability="unavailable"],
5663
+ &[data-availability="unsupported"] {
5664
+ display: none;
5665
+ }
5666
+ }
5667
+
5668
+ & .media-menu__tier {
5669
+ padding-top: 1px;
5670
+ padding-left: 0.125rem;
5671
+ font-size: 0.5625rem;
5672
+ font-weight: 600;
5673
+ line-height: 1;
5674
+ color: oklch(from currentColor l c h / 0.7);
5675
+ }
5676
+
5677
+ & .media-menu__back {
5678
+ width: 100%;
5679
+ margin-bottom: 0.125rem;
5680
+ }
5681
+
5682
+ & .media-menu__hint {
5683
+ display: inline-flex;
5684
+ gap: 0.25rem;
5685
+ align-items: center;
5686
+ min-width: 0;
5687
+ padding-left: 0.5rem;
5688
+ margin-left: auto;
5689
+ font-size: 0.75rem;
5690
+ color: oklch(from currentColor l c h / 0.65);
5691
+ }
5692
+
5693
+ & .media-menu__hint-label {
5694
+ max-width: 6rem;
5695
+ overflow: hidden;
5696
+ text-overflow: ellipsis;
5697
+ white-space: nowrap;
5698
+ }
5699
+
5700
+ & .media-menu__chevron {
5701
+ width: 0.875rem;
5702
+ height: 0.875rem;
5703
+ }
5704
+
5705
+ /* Settings menu */
5706
+ &.media-menu--settings {
5707
+ width: var(--media-menu-width);
5708
+ min-width: 12rem;
5709
+ height: var(--media-menu-height);
5710
+ overflow: hidden;
5711
+ /* Add height and width transitions. */
5712
+ transition:
5713
+ var(--media-popup-transition),
5714
+ height var(--media-popup-transition-timing-function) var(--menu-transition-duration),
5715
+ width var(--media-popup-transition-timing-function) var(--menu-transition-duration);
5716
+
5717
+ /* Don't transition width and height on open/close. */
5718
+ &[data-starting-style],
5719
+ &[data-ending-style] {
5720
+ transition: var(--media-popup-transition);
5721
+ }
5722
+ }
4237
5723
  }
4238
5724
 
4239
5725
  /* ==========================================================================
@@ -4266,8 +5752,8 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4266
5752
  }
4267
5753
 
4268
5754
  /* ==========================================================================
4269
- Input Feedback
4270
- ========================================================================== */
5755
+ Input Feedback
5756
+ ========================================================================== */
4271
5757
 
4272
5758
  .media-minimal-skin .media-input-feedback {
4273
5759
  position: absolute;
@@ -4383,125 +5869,125 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4383
5869
  border-radius: calc(Infinity * 1px);
4384
5870
  box-shadow: 0 1px 0 var(--media-current-shadow-color-subtle);
4385
5871
  }
4386
- }
4387
5872
 
4388
- .media-minimal-skin .media-input-feedback-island--volume[data-level="high"] .media-icon--volume-high,
4389
- .media-minimal-skin .media-input-feedback-island--volume[data-level="low"] .media-icon--volume-low,
4390
- .media-minimal-skin .media-input-feedback-island--volume[data-level="off"] .media-icon--volume-off {
4391
- display: block;
4392
- }
5873
+ &[data-level="high"] .media-icon--volume-high,
5874
+ &[data-level="low"] .media-icon--volume-low,
5875
+ &[data-level="off"] .media-icon--volume-off {
5876
+ display: block;
5877
+ }
4393
5878
 
4394
- .media-minimal-skin .media-input-feedback-island--status[data-status="captions-on"] .media-icon--captions-on,
4395
- .media-minimal-skin .media-input-feedback-island--status[data-status="captions-off"] .media-icon--captions-off,
4396
- .media-minimal-skin .media-input-feedback-island--status[data-status="fullscreen"] .media-icon--fullscreen-enter,
4397
- .media-minimal-skin .media-input-feedback-island--status[data-status="exit-fullscreen"] .media-icon--fullscreen-exit,
4398
- .media-minimal-skin .media-input-feedback-island--status[data-status="pip"] .media-icon--pip-enter,
4399
- .media-minimal-skin .media-input-feedback-island--status[data-status="exit-pip"] .media-icon--pip-exit {
4400
- display: block;
5879
+ @media (prefers-reduced-motion: no-preference) {
5880
+ &[data-min] .media-input-feedback-island__content,
5881
+ &[data-max] .media-input-feedback-island__content {
5882
+ animation: media-shake 300ms ease-in-out;
5883
+ }
5884
+ }
4401
5885
  }
4402
5886
 
4403
- /* --- Boundary shake ------------------------------------------------------- */
4404
-
4405
- @media (prefers-reduced-motion: no-preference) {
4406
- .media-minimal-skin .media-input-feedback-island--volume[data-min] .media-input-feedback-island__content,
4407
- .media-minimal-skin .media-input-feedback-island--volume[data-max] .media-input-feedback-island__content {
4408
- animation: media-shake 300ms ease-in-out;
5887
+ .media-minimal-skin .media-input-feedback-island--status {
5888
+ &[data-status="captions-on"] .media-icon--captions-on,
5889
+ &[data-status="captions-off"] .media-icon--captions-off,
5890
+ &[data-status="fullscreen"] .media-icon--fullscreen-enter,
5891
+ &[data-status="exit-fullscreen"] .media-icon--fullscreen-exit,
5892
+ &[data-status="pip"] .media-icon--pip-enter,
5893
+ &[data-status="exit-pip"] .media-icon--pip-exit {
5894
+ display: block;
4409
5895
  }
4410
5896
  }
4411
5897
 
4412
5898
  /* --- Bubble ---------------------------------------------------------------- */
4413
5899
 
4414
5900
  .media-minimal-skin .media-input-feedback-bubble {
4415
- display: flex;
4416
- flex-direction: column;
5901
+ display: grid;
4417
5902
  grid-row: 1;
4418
- grid-column: 2; /* default to center for status bubbles and undirected seeks */
4419
- align-items: center;
4420
- justify-content: center;
5903
+ grid-column: 2;
5904
+ place-content: center;
4421
5905
  padding: 1rem;
4422
- transition: opacity 250ms ease-out;
4423
-
4424
- @container media-root (width > 24rem) {
4425
- padding: 2rem;
4426
- }
5906
+ text-align: center;
4427
5907
 
4428
- &[data-starting-style],
4429
- &[data-ending-style] {
4430
- opacity: 0;
4431
- transition-timing-function: ease-in;
5908
+ /* Central bubble (play, pause) */
5909
+ &:not([data-direction]) {
5910
+ transition-timing-function: ease-out;
4432
5911
  transition-duration: 200ms;
4433
- }
4434
- }
4435
- /* Direction placement — seek bubbles move to the side implied by their direction. */
4436
- .media-minimal-skin .media-input-feedback-bubble[data-direction="backward"] {
4437
- grid-column: 1;
4438
- justify-self: left;
4439
- }
5912
+ transition-property: opacity, scale;
4440
5913
 
4441
- .media-minimal-skin .media-input-feedback-bubble:not([data-direction]) {
4442
- grid-column: 2;
4443
- transition-timing-function:
4444
- ease-out, linear(0, 0.12 1.5%, 1.35 9.7%, 2.2 13.9%, 3 19.9%, 2.7 21.8%, 0.62 37.5%, 0.96 50.9%, 1);
4445
- transition-duration: 600ms;
4446
- transition-property: opacity, scale;
5914
+ &[data-starting-style],
5915
+ &[data-ending-style] {
5916
+ opacity: 0;
5917
+ scale: 0.85;
5918
+ }
4447
5919
 
4448
- @media (prefers-reduced-motion: reduce) {
4449
- transition: opacity 100ms ease-out;
5920
+ &[data-ending-style] {
5921
+ transition-timing-function: ease-in;
5922
+ transition-duration: 100ms;
5923
+ }
5924
+
5925
+ @media (prefers-reduced-motion: reduce) {
5926
+ transition-duration: 50ms;
5927
+ transition-property: opacity;
5928
+ }
4450
5929
  }
4451
5930
 
4452
- &[data-starting-style],
4453
- &[data-ending-style] {
4454
- opacity: 0;
4455
- scale: 0.8;
4456
- transition-timing-function: ease-in;
4457
- transition-duration: 200ms;
5931
+ /* Directional bubbles (seek) */
5932
+ &[data-direction] {
5933
+ gap: 0.25rem;
5934
+
5935
+ @container media-root (width > 24rem) {
5936
+ padding: 1.5rem;
5937
+ }
4458
5938
  }
4459
- }
4460
5939
 
4461
- .media-minimal-skin .media-input-feedback-bubble[data-direction="forward"] {
4462
- grid-column: 3;
4463
- justify-self: right;
4464
- }
5940
+ &[data-direction="backward"] {
5941
+ grid-column: 1;
5942
+ justify-self: left;
5943
+ }
4465
5944
 
4466
- /* --- Bubble icons ---------------------------------------------------------- */
5945
+ &[data-direction="forward"] {
5946
+ grid-column: 3;
5947
+ justify-self: right;
5948
+ }
4467
5949
 
4468
- .media-minimal-skin .media-input-feedback-bubble .media-icon {
4469
- display: none;
4470
- width: 36px;
4471
- height: 36px;
4472
- }
5950
+ /* Icons */
5951
+ .media-icon {
5952
+ display: none;
5953
+ width: calc(var(--media-icon-size) * 2);
5954
+ height: calc(var(--media-icon-size) * 2);
5955
+ }
4473
5956
 
4474
- /* seek: seek icon, flipped for backward */
4475
- .media-minimal-skin .media-input-feedback-bubble[data-direction] .media-icon--seek {
4476
- display: block;
4477
- }
5957
+ .media-icon--seek {
5958
+ width: calc(var(--media-icon-size) * 1.5);
5959
+ height: calc(var(--media-icon-size) * 1.5);
5960
+ }
4478
5961
 
4479
- .media-minimal-skin .media-input-feedback-bubble[data-direction="backward"] .media-icon--seek {
4480
- transform: scaleX(-1);
4481
- }
5962
+ &[data-direction] .media-icon--seek,
5963
+ &[data-status="pause"] .media-icon--pause,
5964
+ &[data-status="play"] .media-icon--play {
5965
+ display: block;
5966
+ }
4482
5967
 
4483
- @media (prefers-reduced-motion: no-preference) {
4484
- .media-minimal-skin
4485
- .media-input-feedback-bubble[data-direction="forward"]:not([data-starting-style])
4486
- .media-icon--seek {
4487
- animation: media-slide-in-forward 300ms ease-in-out;
5968
+ &[data-direction="backward"] .media-icon--seek {
5969
+ scale: -1 1;
4488
5970
  }
4489
5971
 
4490
- .media-minimal-skin
4491
- .media-input-feedback-bubble[data-direction="backward"]:not([data-starting-style])
5972
+ @media (prefers-reduced-motion: no-preference) {
4492
5973
  .media-icon--seek {
4493
- animation: media-slide-in-backward 300ms ease-in-out;
4494
- }
5974
+ transition-timing-function: ease-in-out;
5975
+ transition-duration: 200ms;
5976
+ transition-property: translate, opacity;
5977
+ }
4495
5978
 
4496
- .media-minimal-skin .media-input-feedback-island--status[data-status]:not([data-starting-style]) .media-icon,
4497
- .media-minimal-skin .media-input-feedback-bubble[data-status]:not([data-starting-style]) .media-icon {
4498
- animation: media-pop-in 250ms ease-out;
4499
- }
4500
- }
5979
+ &[data-starting-style] .media-icon--seek,
5980
+ &[data-ending-style] .media-icon--seek {
5981
+ opacity: 0;
5982
+ }
4501
5983
 
4502
- .media-minimal-skin .media-input-feedback-bubble[data-status="pause"] .media-icon--pause,
4503
- .media-minimal-skin .media-input-feedback-bubble[data-status="play"] .media-icon--play {
4504
- display: block;
5984
+ &[data-direction="forward"][data-starting-style] .media-icon--seek {
5985
+ translate: -60% 0;
5986
+ }
5987
+ &[data-direction="backward"][data-starting-style] .media-icon--seek {
5988
+ translate: 60% 0;
5989
+ }
5990
+ }
4505
5991
  }
4506
5992
 
4507
5993
  /* ==========================================================================
@@ -4526,6 +6012,8 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4526
6012
  .media-button--pip .media-icon--pip-exit,
4527
6013
  .media-button--cast .media-icon--cast-enter,
4528
6014
  .media-button--cast .media-icon--cast-exit,
6015
+ .media-button--airplay .media-icon--airplay-enter,
6016
+ .media-button--airplay .media-icon--airplay-exit,
4529
6017
  .media-button--captions .media-icon--captions-off,
4530
6018
  .media-button--captions .media-icon--captions-on {
4531
6019
  display: none;
@@ -4536,10 +6024,11 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4536
6024
 
4537
6025
  /* Play: ended → restart */
4538
6026
  .media-button--play[data-ended] .media-icon--restart,
4539
- /* Play: paused (not ended) → play */
6027
+ /* Play: paused or not yet started (not ended) → play */
4540
6028
  .media-button--play:not([data-ended])[data-paused] .media-icon--play,
4541
- /* Play: playing (not paused, not ended) → pause */
4542
- .media-button--play:not([data-paused]):not([data-ended]) .media-icon--pause,
6029
+ .media-button--play:not([data-ended]):not([data-started]) .media-icon--play,
6030
+ /* Play: started and not paused/ended pause */
6031
+ .media-button--play[data-started]:not([data-paused]):not([data-ended]) .media-icon--pause,
4543
6032
  /* Mute: muted → volume off */
4544
6033
  .media-button--mute[data-muted] .media-icon--volume-off,
4545
6034
  /* Mute: volume low (not muted) → volume low */
@@ -4558,6 +6047,10 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4558
6047
  .media-button--cast:not([data-cast-state="connected"]) .media-icon--cast-enter,
4559
6048
  /* Cast: connected → exit */
4560
6049
  .media-button--cast[data-cast-state="connected"] .media-icon--cast-exit,
6050
+ /* AirPlay: not connected → enter */
6051
+ .media-button--airplay:not([data-airplay-state="connected"]) .media-icon--airplay-enter,
6052
+ /* AirPlay: connected → exit */
6053
+ .media-button--airplay[data-airplay-state="connected"] .media-icon--airplay-exit,
4561
6054
  /* Captions: not active → captions off */
4562
6055
  .media-button--captions:not([data-active]) .media-icon--captions-off,
4563
6056
  /* Captions: active → captions on */
@@ -4566,6 +6059,17 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4566
6059
  opacity: 1;
4567
6060
  }
4568
6061
 
6062
+ /* --- Pause keyframe animations on inactive icons --- */
6063
+
6064
+ /* The airplay-exit SVG defines its keyframes against CSS variables (mirroring
6065
+ the spinner pattern). When the AirPlay session isn't active the SVG is
6066
+ still in the DOM — just `display: none` — so its animations would keep
6067
+ running. Set the variables to `none` to short-circuit the keyframes. */
6068
+ .media-button--airplay:not([data-airplay-state="connected"]) {
6069
+ --media-icon--airplay__fill-animation: none;
6070
+ --media-icon--airplay__triangle-animation: none;
6071
+ }
6072
+
4569
6073
  /* -------------------------------------------------------------------------- */
4570
6074
  /* Global @keyframes for all video skins (CSS & Tailwind) */
4571
6075
  /* -------------------------------------------------------------------------- */
@@ -4589,27 +6093,6 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4589
6093
  }
4590
6094
  }
4591
6095
 
4592
- @keyframes media-slide-in-forward {
4593
- from {
4594
- translate: -60% 0;
4595
- opacity: 0;
4596
- }
4597
- }
4598
-
4599
- @keyframes media-slide-in-backward {
4600
- from {
4601
- translate: 60% 0;
4602
- opacity: 0;
4603
- }
4604
- }
4605
-
4606
- @keyframes media-pop-in {
4607
- from {
4608
- scale: 0.8;
4609
- opacity: 0;
4610
- }
4611
- }
4612
-
4613
6096
  /* -------------------------------------------------------------------------- */
4614
6097
  /* Global @properties for all video skins (CSS & Tailwind) */
4615
6098
  /* -------------------------------------------------------------------------- */
@@ -4636,14 +6119,17 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4636
6119
  --media-error-dialog-transition-timing-function: ease-out;
4637
6120
  --media-popup-transition-duration: 100ms;
4638
6121
  --media-popup-transition-timing-function: ease-out;
4639
- --media-tooltip-background-color: oklch(1 0 0 / 0.1);
4640
- --media-tooltip-border-color: transparent;
6122
+ --media-tooltip-background-color: oklch(0 0 0 / 0.5);
6123
+ --media-tooltip-border-color: oklch(1 0 0 / 0.1);
4641
6124
  --media-tooltip-backdrop-filter: blur(16px) saturate(1.5);
4642
6125
  --media-tooltip-text-color: currentColor;
4643
6126
  --media-tooltip-side-offset: 0.5rem;
4644
6127
  --media-tooltip-boundary-offset: 0.5rem;
6128
+ --media-popover-background-color: var(--media-tooltip-background-color);
6129
+ --media-popover-border-color: var(--media-tooltip-border-color);
6130
+ --media-popover-backdrop-filter: var(--media-tooltip-backdrop-filter);
4645
6131
  --media-popover-side-offset: 1.5rem;
4646
- --media-popover-boundary-offset: 0.5rem;
6132
+ --media-popover-boundary-offset: var(--media-tooltip-boundary-offset);
4647
6133
  overflow: clip;
4648
6134
  background: oklch(0 0 0);
4649
6135
 
@@ -4763,9 +6249,7 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4763
6249
  z-index: 10;
4764
6250
  flex-wrap: wrap;
4765
6251
  column-gap: 0.5rem;
4766
- max-width: 56rem;
4767
6252
  padding: 0.25rem;
4768
- margin-inline: auto;
4769
6253
  color: oklch(1 0 0);
4770
6254
  border-radius: 0.75rem;
4771
6255
  transition-timing-function: var(--media-controls-transition-timing-function);
@@ -4864,21 +6348,28 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4864
6348
  Slider preview
4865
6349
  ========================================================================== */
4866
6350
 
4867
- .media-minimal-skin--video .media-slider__preview {
4868
- --media-preview-max-width: 11rem;
4869
- --media-preview-padding: -0.5rem;
6351
+ .media-minimal-skin--video .media-slider__thumbnail {
6352
+ --media-slider-thumbnail-max-width: 11rem;
6353
+ --media-slider-thumbnail-max-height: 8rem;
6354
+ --media-slider-thumbnail-padding: -0.5rem;
4870
6355
  /**
4871
6356
  Inset is the difference between the container width and the slider (100%) width.
4872
6357
  We only add to the end as we render the time there.
4873
6358
  */
4874
- --media-preview-inset: calc(100cqi - 100%);
6359
+ --media-slider-thumbnail-inset: calc(100cqi - 100%);
4875
6360
 
4876
6361
  position: absolute;
4877
6362
  bottom: 100%;
4878
6363
  left: clamp(
4879
- calc(var(--media-preview-max-width) / 2 + var(--media-preview-padding)),
6364
+ calc(var(--media-slider-thumbnail-max-width) / 2 + var(--media-slider-thumbnail-padding)),
4880
6365
  var(--media-slider-pointer),
4881
- calc(100% - var(--media-preview-max-width) / 2 - var(--media-preview-padding) + var(--media-preview-inset))
6366
+ calc(
6367
+ 100% -
6368
+ var(--media-slider-thumbnail-max-width) /
6369
+ 2 -
6370
+ var(--media-slider-thumbnail-padding) +
6371
+ var(--media-slider-thumbnail-inset)
6372
+ )
4882
6373
  );
4883
6374
  opacity: 0;
4884
6375
  filter: blur(8px);
@@ -4890,11 +6381,10 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4890
6381
  transition-property: scale, opacity, filter;
4891
6382
 
4892
6383
  @container media-root (width > 42rem) {
4893
- bottom: calc(100% + 0.25rem);
4894
6384
  left: var(--media-slider-pointer);
4895
6385
  }
4896
6386
 
4897
- & .media-preview__thumbnail-wrapper {
6387
+ & .media-thumbnail__image-wrapper {
4898
6388
  position: relative;
4899
6389
 
4900
6390
  &::after {
@@ -4909,19 +6399,22 @@ function isRenderProp(value: unknown): value is RenderProp<unknown> {
4909
6399
  }
4910
6400
  }
4911
6401
 
4912
- & .media-preview__thumbnail {
4913
- max-width: var(--media-preview-max-width);
4914
- }
4915
-
4916
- &:has(.media-preview__thumbnail[data-loading]) {
4917
- max-height: 6rem;
6402
+ & .media-thumbnail__image {
6403
+ max-width: var(--media-slider-thumbnail-max-width);
6404
+ max-height: var(--media-slider-thumbnail-max-height);
4918
6405
  }
4919
6406
  }
4920
- .media-minimal-skin--video .media-slider[data-pointing] .media-slider__preview:has([role="img"]:not([data-hidden])) {
6407
+ .media-minimal-skin--video .media-slider[data-pointing] .media-slider__thumbnail:has([role="img"]:not([data-hidden])) {
4921
6408
  opacity: 1;
4922
6409
  filter: blur(0);
4923
6410
  scale: 1;
4924
6411
  }
6412
+ .media-minimal-skin--video
6413
+ .media-slider__thumbnail:has([role="img"]:not([data-hidden]))
6414
+ + .media-slider__preview
6415
+ .media-slider__value {
6416
+ display: none;
6417
+ }
4925
6418
 
4926
6419
  ```
4927
6420
 
@@ -4935,16 +6428,19 @@ skin.css
4935
6428
  'use client';
4936
6429
 
4937
6430
  import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode } from 'react';
4938
- import { CheckIcon, PauseIcon, PlayIcon, RestartIcon, SeekIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons/minimal';
4939
- import { createPlayer, Container, usePlayer, ErrorDialog, Hotkey, Menu, MuteButton, PlayButton, PlaybackRateMenu, usePlaybackRateMenu, Popover, SeekButton, StatusAnnouncer, Time, TimeSlider, Tooltip, VolumeSlider, type Poster, type RenderProp } from '@videojs/react';
6431
+ import { CheckIcon, PauseIcon, PlayIcon, RestartIcon, SeekIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons/minimal';
6432
+ import { createPlayer, Container, usePlayer, BufferingIndicator, ErrorDialog, Hotkey, Menu, MuteButton, PlayButton, usePlaybackRateOptions, PlaybackRateButton, Popover, SeekButton, StatusAnnouncer, Time, TimeSlider, Tooltip, VolumeSlider, type Poster, type RenderProp } from '@videojs/react';
4940
6433
  import { Audio, audioFeatures } from '@videojs/react/audio';
4941
6434
  import './player.css';
4942
6435
 
4943
- function PlaybackRateMenuItems(): ReactNode {
4944
- const { options, setValue, value } = usePlaybackRateMenu();
6436
+ function PlaybackRateRadioGroup(): ReactNode {
6437
+ const state = usePlaybackRateOptions();
6438
+ if (!state) return null;
6439
+
6440
+ const { options, setValue, value } = state;
4945
6441
 
4946
6442
  return (
4947
- <Menu.RadioGroup className="media-menu__group" value={value} onValueChange={setValue} label="Playback rate">
6443
+ <Menu.RadioGroup className="media-menu__group" value={value} onValueChange={setValue} aria-label="Playback rate">
4948
6444
  {options.map((option) => (
4949
6445
  <Menu.RadioItem key={option.value} className="media-menu__item" value={option.value} disabled={option.disabled}>
4950
6446
  <span>{option.label}</span>
@@ -4957,6 +6453,18 @@ function PlaybackRateMenuItems(): ReactNode {
4957
6453
  );
4958
6454
  }
4959
6455
 
6456
+ function PlaybackRateTrigger(): ReactNode {
6457
+ const state = usePlaybackRateOptions();
6458
+ if (!state) return null;
6459
+
6460
+ return (
6461
+ <Menu.Trigger
6462
+ disabled={state.disabled}
6463
+ render={<PlaybackRateButton className="media-button--playback-rate" render={<Button />} />}
6464
+ />
6465
+ );
6466
+ }
6467
+
4960
6468
  // ================================================================
4961
6469
  // Player
4962
6470
  // ================================================================
@@ -5002,18 +6510,30 @@ export function AudioPlayer({ src, className, ...rest }: AudioPlayerProps): Reac
5002
6510
  <div className="media-controls">
5003
6511
  <Tooltip.Provider>
5004
6512
  <div className="media-button-group">
5005
- <Tooltip.Root side="top" boundary="viewport">
5006
- <Tooltip.Trigger
5007
- render={
5008
- <PlayButton className="media-button--play" render={<Button />}>
5009
- <RestartIcon className="media-icon media-icon--restart" />
5010
- <PlayIcon className="media-icon media-icon--play" />
5011
- <PauseIcon className="media-icon media-icon--pause" />
5012
- </PlayButton>
5013
- }
6513
+ <span className="media-button--play__wrapper">
6514
+ <BufferingIndicator
6515
+ render={(props) => (
6516
+ <div {...props} className="media-buffering-indicator">
6517
+ <SpinnerIcon className="media-icon" />
6518
+ </div>
6519
+ )}
5014
6520
  />
5015
- <Tooltip.Popup className="media-tooltip" />
5016
- </Tooltip.Root>
6521
+ <Tooltip.Root side="top" boundary="viewport">
6522
+ <Tooltip.Trigger
6523
+ render={
6524
+ <PlayButton className="media-button--play" render={<Button />}>
6525
+ <RestartIcon className="media-icon media-icon--restart" />
6526
+ <PlayIcon className="media-icon media-icon--play" />
6527
+ <PauseIcon className="media-icon media-icon--pause" />
6528
+ </PlayButton>
6529
+ }
6530
+ />
6531
+ <Tooltip.Popup className="media-tooltip">
6532
+ <Tooltip.Label />
6533
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
6534
+ </Tooltip.Popup>
6535
+ </Tooltip.Root>
6536
+ </span>
5017
6537
 
5018
6538
  <Tooltip.Root side="top" boundary="viewport">
5019
6539
  <Tooltip.Trigger
@@ -5026,7 +6546,10 @@ export function AudioPlayer({ src, className, ...rest }: AudioPlayerProps): Reac
5026
6546
  </SeekButton>
5027
6547
  }
5028
6548
  />
5029
- <Tooltip.Popup className="media-tooltip">Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
6549
+ <Tooltip.Popup className="media-tooltip">
6550
+ <Tooltip.Label />
6551
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
6552
+ </Tooltip.Popup>
5030
6553
  </Tooltip.Root>
5031
6554
 
5032
6555
  <Tooltip.Root side="top" boundary="viewport">
@@ -5040,13 +6563,16 @@ export function AudioPlayer({ src, className, ...rest }: AudioPlayerProps): Reac
5040
6563
  </SeekButton>
5041
6564
  }
5042
6565
  />
5043
- <Tooltip.Popup className="media-tooltip">Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
6566
+ <Tooltip.Popup className="media-tooltip">
6567
+ <Tooltip.Label />
6568
+ <Tooltip.Shortcut className="media-tooltip__kbd" />
6569
+ </Tooltip.Popup>
5044
6570
  </Tooltip.Root>
5045
6571
  </div>
5046
6572
 
5047
6573
  <div className="media-time-controls">
5048
6574
  <Time.Group className="media-time-group">
5049
- <Time.Value type="current" className="media-time media-time--current" />
6575
+ <Time.Value toggle type="current" className="media-time media-time--current" />
5050
6576
  <Time.Separator className="media-time-separator" />
5051
6577
  <Time.Value type="duration" className="media-time media-time--duration" />
5052
6578
  </Time.Group>
@@ -5057,16 +6583,19 @@ export function AudioPlayer({ src, className, ...rest }: AudioPlayerProps): Reac
5057
6583
  <TimeSlider.Buffer className="media-slider__buffer" />
5058
6584
  </TimeSlider.Track>
5059
6585
  <TimeSlider.Thumb className="media-slider__thumb" />
6586
+ <TimeSlider.Preview className="media-slider__preview">
6587
+ <TimeSlider.Value type="pointer" className="media-slider__value media-time" />
6588
+ </TimeSlider.Preview>
5060
6589
  </TimeSlider.Root>
5061
6590
  </div>
5062
6591
 
5063
6592
  <div className="media-button-group">
5064
- <PlaybackRateMenu.Root side="top" align="center" boundary="viewport">
5065
- <PlaybackRateMenu.Trigger className="media-button--playback-rate" render={<Button />} />
5066
- <PlaybackRateMenu.Content className="media-popover media-menu media-menu--playback-rate">
5067
- <PlaybackRateMenuItems />
5068
- </PlaybackRateMenu.Content>
5069
- </PlaybackRateMenu.Root>
6593
+ <Menu.Root side="top" align="center" boundary="viewport">
6594
+ <PlaybackRateTrigger />
6595
+ <Menu.Content className="media-popover media-menu media-menu--playback-rate">
6596
+ <PlaybackRateRadioGroup />
6597
+ </Menu.Content>
6598
+ </Menu.Root>
5070
6599
 
5071
6600
  <VolumePopover />
5072
6601
  </div>
@@ -5177,6 +6706,7 @@ function VolumePopover(): ReactNode {
5177
6706
  --media-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.15));
5178
6707
  --media-current-shadow-color-subtle: oklch(from var(--media-current-shadow-color) l c h / calc(alpha * 0.4));
5179
6708
  --media-icon-size: 18px;
6709
+ --media-color-scrollbar-thumb: oklch(from currentColor l c h / 0.3);
5180
6710
  position: relative;
5181
6711
  display: block;
5182
6712
  width: 100%;
@@ -5195,6 +6725,8 @@ function VolumePopover(): ReactNode {
5195
6725
  letter-spacing: normal;
5196
6726
  outline: 2px solid transparent;
5197
6727
  outline-offset: -4px;
6728
+ scrollbar-color: var(--media-color-scrollbar-thumb) transparent;
6729
+ scrollbar-width: thin;
5198
6730
  border-radius: var(--media-border-radius, 0.75rem);
5199
6731
  isolation: isolate;
5200
6732
  transition-timing-function: ease-out;
@@ -5205,6 +6737,16 @@ function VolumePopover(): ReactNode {
5205
6737
  outline-color: currentColor;
5206
6738
  outline-offset: 2px;
5207
6739
  }
6740
+
6741
+ &::-webkit-scrollbar-thumb {
6742
+ background: var(--media-color-scrollbar-thumb);
6743
+ border-radius: 9999px;
6744
+ }
6745
+
6746
+ @media (prefers-reduced-transparency: reduce) or (prefers-contrast: more) {
6747
+ --media-color-scrollbar-thumb: oklch(from currentColor l c h / 0.8);
6748
+ scrollbar-width: auto;
6749
+ }
5208
6750
  }
5209
6751
 
5210
6752
  /* ==========================================================================
@@ -5214,9 +6756,9 @@ function VolumePopover(): ReactNode {
5214
6756
  .media-minimal-skin .media-buffering-indicator {
5215
6757
  position: absolute;
5216
6758
  inset: 0;
6759
+ z-index: 10;
5217
6760
  display: none;
5218
- align-items: center;
5219
- justify-content: center;
6761
+ place-content: center;
5220
6762
  color: oklch(1 0 0);
5221
6763
  pointer-events: none;
5222
6764
 
@@ -5225,7 +6767,7 @@ function VolumePopover(): ReactNode {
5225
6767
  }
5226
6768
 
5227
6769
  &[data-visible] {
5228
- display: flex;
6770
+ display: grid;
5229
6771
  }
5230
6772
  }
5231
6773
 
@@ -5296,6 +6838,26 @@ function VolumePopover(): ReactNode {
5296
6838
  font-variant-numeric: tabular-nums;
5297
6839
  }
5298
6840
 
6841
+ .media-minimal-skin .media-time[role="button"] {
6842
+ cursor: pointer;
6843
+ outline: 2px solid transparent;
6844
+ outline-offset: -2px;
6845
+ border-radius: 0.25rem;
6846
+ transition-timing-function: ease-out;
6847
+ transition-duration: 100ms;
6848
+ transition-property: outline-color, outline-offset;
6849
+
6850
+ @supports (corner-shape: squircle) {
6851
+ border-radius: 1rem;
6852
+ corner-shape: squircle;
6853
+ }
6854
+
6855
+ &:focus-visible {
6856
+ outline-color: currentColor;
6857
+ outline-offset: 2px;
6858
+ }
6859
+ }
6860
+
5299
6861
  .media-minimal-skin .media-time--current,
5300
6862
  .media-minimal-skin .media-time-separator {
5301
6863
  display: none;
@@ -5403,6 +6965,10 @@ function VolumePopover(): ReactNode {
5403
6965
  scale: 0.9;
5404
6966
  }
5405
6967
 
6968
+ & .media-icon__container {
6969
+ display: grid;
6970
+ }
6971
+
5406
6972
  & .media-icon {
5407
6973
  grid-area: 1 / 1;
5408
6974
  transition-behavior: allow-discrete;
@@ -5420,8 +6986,9 @@ function VolumePopover(): ReactNode {
5420
6986
  right: -1px;
5421
6987
  bottom: -3px;
5422
6988
  font-size: 10px; /* Hard coded due to size limitations. */
5423
- font-weight: 480;
6989
+ font-weight: 500;
5424
6990
  font-variant-numeric: tabular-nums;
6991
+ letter-spacing: -0.05em;
5425
6992
  }
5426
6993
 
5427
6994
  &:has(.media-icon--flipped) .media-icon__label {
@@ -5445,6 +7012,27 @@ function VolumePopover(): ReactNode {
5445
7012
  }
5446
7013
  }
5447
7014
 
7015
+ /* Settings button */
7016
+ .media-minimal-skin .media-button--settings {
7017
+ display: none;
7018
+
7019
+ & .media-icon--settings {
7020
+ transition: transform 150ms ease-in-out;
7021
+
7022
+ @media (prefers-reduced-motion: reduce) {
7023
+ transition-duration: 0ms;
7024
+ }
7025
+ }
7026
+
7027
+ &[aria-expanded="true"] .media-icon--settings {
7028
+ transform: rotate(90deg);
7029
+ }
7030
+ }
7031
+
7032
+ .media-minimal-skin .media-button-group:has([data-availability="available"]) .media-button--settings {
7033
+ display: grid;
7034
+ }
7035
+
5448
7036
  /* Live button — wide pill button with a status dot (gray → red at the live
5449
7037
  edge) rendered via ::before, and "LIVE" text rendered as the button's own
5450
7038
  text content. */
@@ -5491,6 +7079,21 @@ function VolumePopover(): ReactNode {
5491
7079
  }
5492
7080
  }
5493
7081
 
7082
+ /* ==========================================================================
7083
+ Badge
7084
+ ========================================================================== */
7085
+
7086
+ .media-minimal-skin .media-badge {
7087
+ padding: 0.25rem 0.375rem;
7088
+ font-size: 0.6875rem;
7089
+ font-weight: 500;
7090
+ line-height: 1;
7091
+ color: oklch(from currentColor l c h / 0.85);
7092
+ white-space: nowrap;
7093
+ background-color: oklch(from currentColor l c h / 0.1);
7094
+ border-radius: 0.25rem;
7095
+ }
7096
+
5494
7097
  /* ==========================================================================
5495
7098
  Icons
5496
7099
  ========================================================================== */
@@ -5498,6 +7101,7 @@ function VolumePopover(): ReactNode {
5498
7101
  .media-minimal-skin .media-icon__container {
5499
7102
  position: relative;
5500
7103
  }
7104
+
5501
7105
  .media-minimal-skin .media-icon {
5502
7106
  flex-shrink: 0;
5503
7107
  width: var(--media-icon-size);
@@ -5565,7 +7169,7 @@ function VolumePopover(): ReactNode {
5565
7169
  background-color: currentColor;
5566
7170
  border-radius: calc(infinity * 1px);
5567
7171
  box-shadow:
5568
- 0 0 0 1px var(--media-current-shadow-color-subtle, oklch(0 0 0 / 0.1)),
7172
+ 0 0 0 1px var(--media-current-shadow-color, oklch(0 0 0 / 0.15)),
5569
7173
  0 1px 3px 0 oklch(0 0 0 / 0.15),
5570
7174
  0 1px 2px -1px oklch(0 0 0 / 0.15);
5571
7175
  opacity: 0;
@@ -5599,6 +7203,53 @@ function VolumePopover(): ReactNode {
5599
7203
  scale: 1;
5600
7204
  }
5601
7205
 
7206
+ /* Preview */
7207
+ .media-minimal-skin .media-slider__preview {
7208
+ & .media-slider__value,
7209
+ &::before {
7210
+ opacity: 0;
7211
+ scale: 0.5;
7212
+ transition-timing-function: ease-out;
7213
+ transition-duration: 200ms;
7214
+ transition-property: opacity, scale;
7215
+ }
7216
+
7217
+ & .media-slider__value {
7218
+ position: absolute;
7219
+ bottom: 1.5rem;
7220
+ text-shadow: 0 1px 0 var(--media-current-shadow-color);
7221
+ filter: blur(8px);
7222
+ translate: -50% 0.5rem;
7223
+ transition-property: filter, opacity, scale, translate;
7224
+ }
7225
+
7226
+ &::before {
7227
+ display: block;
7228
+ content: "";
7229
+ background-color: oklch(from currentColor l c h / 0.35);
7230
+ }
7231
+
7232
+ &[data-pointing] .media-slider__value,
7233
+ &[data-pointing]:not([data-dragging])::before {
7234
+ opacity: 1;
7235
+ scale: 1;
7236
+ }
7237
+ &[data-pointing] .media-slider__value {
7238
+ filter: blur(0);
7239
+ translate: -50% 0;
7240
+ }
7241
+
7242
+ &[data-orientation="horizontal"]::before {
7243
+ min-width: 1px;
7244
+ height: 1.25rem;
7245
+ }
7246
+
7247
+ &[data-orientation="vertical"]::before {
7248
+ width: 1.25rem;
7249
+ min-height: 1px;
7250
+ }
7251
+ }
7252
+
5602
7253
  /* Shared track fills */
5603
7254
  .media-minimal-skin .media-slider__buffer,
5604
7255
  .media-minimal-skin .media-slider__fill {
@@ -5670,37 +7321,52 @@ function VolumePopover(): ReactNode {
5670
7321
  Popups & Animations
5671
7322
  ========================================================================== */
5672
7323
 
7324
+ .media-minimal-skin {
7325
+ --media-popup-transition:
7326
+ opacity var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
7327
+ filter var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
7328
+ transform var(--media-popup-transition-timing-function) var(--media-popup-transition-duration),
7329
+ scale var(--media-popup-transition-timing-function) var(--media-popup-transition-duration);
7330
+ }
7331
+
5673
7332
  .media-minimal-skin .media-popover,
5674
7333
  .media-minimal-skin .media-tooltip {
7334
+ --popup-translate-distance: 0.5rem;
5675
7335
  margin: 0;
5676
7336
  overflow: visible;
5677
7337
  color: inherit;
5678
7338
  border: 0;
5679
- transition-timing-function: var(--media-popup-transition-timing-function);
5680
- transition-duration: var(--media-popup-transition-duration);
5681
- transition-property: scale, opacity, filter;
7339
+ transition: var(--media-popup-transition);
5682
7340
 
5683
7341
  &[data-starting-style],
5684
7342
  &[data-ending-style] {
5685
7343
  opacity: 0;
5686
- filter: blur(8px);
5687
- scale: 0.5;
7344
+ filter: blur(4px);
7345
+ /* We have to use transform here for translate as the translate property is used for positioning by core. */
7346
+ transform: translate(var(--popup-translate-x-distance, 0), var(--popup-translate-y-distance, 0));
7347
+ scale: 0.95;
5688
7348
  }
5689
7349
 
5690
- &[data-instant] {
5691
- transition-duration: 0ms;
7350
+ &[data-ending-style] {
7351
+ transform: none;
7352
+ /* Speed up the exit transition. */
7353
+ transition-duration: max(0ms, calc(var(--media-popup-transition-duration) - 50ms));
5692
7354
  }
5693
7355
 
5694
7356
  &[data-side="top"] {
7357
+ --popup-translate-y-distance: var(--popup-translate-distance);
5695
7358
  transform-origin: bottom;
5696
7359
  }
5697
7360
  &[data-side="bottom"] {
7361
+ --popup-translate-y-distance: calc(var(--popup-translate-distance) * -1);
5698
7362
  transform-origin: top;
5699
7363
  }
5700
7364
  &[data-side="left"] {
7365
+ --popup-translate-x-distance: var(--popup-translate-distance);
5701
7366
  transform-origin: right;
5702
7367
  }
5703
7368
  &[data-side="right"] {
7369
+ --popup-translate-x-distance: calc(var(--popup-translate-distance) * -1);
5704
7370
  transform-origin: left;
5705
7371
  }
5706
7372
 
@@ -5756,10 +7422,17 @@ function VolumePopover(): ReactNode {
5756
7422
  border-radius: 0.5rem;
5757
7423
  box-shadow:
5758
7424
  0 0 0 1px var(--media-tooltip-border-color),
5759
- 0 4px 6px -1px oklch(0 0 0 / 0.1),
5760
- 0 2px 4px -2px oklch(0 0 0 / 0.1);
7425
+ 0 4px 6px -1px oklch(0 0 0 / 0.2),
7426
+ 0 2px 4px -2px oklch(0 0 0 / 0.2);
5761
7427
  backdrop-filter: var(--media-tooltip-backdrop-filter);
5762
7428
 
7429
+ /* `display: flex` must not apply while closed — it overrides UA `[popover]` hiding. */
7430
+ &[data-open] {
7431
+ display: flex;
7432
+ column-gap: 0.25rem;
7433
+ align-items: center;
7434
+ }
7435
+
5763
7436
  &[data-side="top"]::before,
5764
7437
  &[data-side="bottom"]::before {
5765
7438
  height: var(--media-tooltip-side-offset);
@@ -5768,6 +7441,19 @@ function VolumePopover(): ReactNode {
5768
7441
  &[data-side="right"]::before {
5769
7442
  width: var(--media-tooltip-side-offset);
5770
7443
  }
7444
+
7445
+ & .media-tooltip__kbd {
7446
+ min-width: 1.5em;
7447
+ padding: 0.1em;
7448
+ margin-right: -0.25rem;
7449
+ font-family: inherit;
7450
+ font-size: 90%;
7451
+ font-weight: 600;
7452
+ line-height: 1.25;
7453
+ text-align: center;
7454
+ background-color: oklch(from currentColor l c h / 0.15);
7455
+ border-radius: 0.25rem;
7456
+ }
5771
7457
  }
5772
7458
 
5773
7459
  .media-minimal-skin .media-popover--volume:has(media-volume-slider[data-availability="unsupported"]) {
@@ -5779,72 +7465,232 @@ function VolumePopover(): ReactNode {
5779
7465
  Note: Menus use `.media-popover` styles for positioning and transitions.
5780
7466
  ========================================================================== */
5781
7467
 
5782
- .media-minimal-skin .media-popover.media-menu {
7468
+ .media-minimal-skin .media-menu {
7469
+ --menu-transition-duration: 250ms;
7470
+ --menu-max-height: 14rem;
7471
+ --menu-padding: 0.5rem;
7472
+ --menu-border-radius: 1rem;
7473
+ --menu-item-border-radius: calc(var(--menu-border-radius) - var(--menu-padding));
5783
7474
  box-sizing: border-box;
5784
- min-width: min(6rem, var(--media-popover-available-width, 6rem));
5785
7475
  max-width: var(--media-popover-available-width, none);
5786
- max-height: var(--media-popover-available-height, none);
5787
- padding: 0.25rem;
7476
+ max-height: min(var(--media-popover-available-height, var(--menu-max-height)), var(--menu-max-height));
7477
+ padding: var(--menu-padding);
5788
7478
  overflow: auto;
5789
7479
  overscroll-behavior: none;
5790
- background-color: var(--media-tooltip-background-color);
5791
- border-radius: 0.75rem;
7480
+ background-color: var(--media-popover-background-color);
7481
+ border-radius: var(--menu-border-radius);
5792
7482
  box-shadow:
5793
- 0 0 0 1px var(--media-tooltip-border-color),
7483
+ 0 0 0 1px var(--media-popover-border-color),
5794
7484
  0 4px 6px -1px oklch(0 0 0 / 0.1),
5795
7485
  0 2px 4px -2px oklch(0 0 0 / 0.1);
5796
- backdrop-filter: var(--media-tooltip-backdrop-filter);
7486
+ backdrop-filter: var(--media-popover-backdrop-filter);
5797
7487
 
5798
- &::before {
5799
- display: none;
7488
+ @media (prefers-reduced-motion: reduce) {
7489
+ --menu-transition-duration: 0ms;
5800
7490
  }
5801
- }
5802
7491
 
5803
- .media-minimal-skin .media-popover.media-menu .media-menu__group {
5804
- display: flex;
5805
- flex-direction: column;
5806
- gap: 0.125rem;
5807
- }
7492
+ & .media-menu__panel {
7493
+ position: absolute;
7494
+ inset: 0;
7495
+ padding: var(--menu-padding);
7496
+ overflow: auto;
7497
+ overscroll-behavior: none;
7498
+ outline: none;
7499
+ translate: 0 0;
7500
+ transition-timing-function: ease-in-out;
7501
+ transition-duration: var(--menu-transition-duration);
7502
+ transition-property: translate, filter;
7503
+ will-change: translate, filter;
7504
+
7505
+ &[data-starting-style],
7506
+ &[data-ending-style] {
7507
+ overflow: hidden;
7508
+ }
5808
7509
 
5809
- .media-minimal-skin .media-popover.media-menu .media-menu__item {
5810
- display: flex;
5811
- gap: 0.5rem;
5812
- align-items: center;
5813
- justify-content: space-between;
5814
- min-height: 2rem;
5815
- padding: 0 0.625rem;
5816
- font-variant-numeric: tabular-nums;
5817
- color: inherit;
5818
- cursor: pointer;
5819
- outline: 2px solid transparent;
5820
- outline-offset: -2px;
5821
- border-radius: 0.5rem;
7510
+ /* Root settings view — slides out when a submenu is active */
7511
+ &[data-menu-root-view][data-menu-view-state="inactive"] {
7512
+ filter: blur(8px);
7513
+ translate: -100% 0;
5822
7514
 
5823
- &:hover,
5824
- &[data-highlighted] {
5825
- background-color: oklch(from currentColor l c h / 0.1);
7515
+ /* Hide the indicator while transitioning */
7516
+ & .media-menu__group::before {
7517
+ display: none;
7518
+ }
7519
+ }
7520
+
7521
+ /* Submenu panels — slide in/out alongside the root view */
7522
+ &[data-submenu] {
7523
+ z-index: 10;
7524
+
7525
+ &:not([data-open], [data-ending-style]) {
7526
+ translate: -100% 0;
7527
+ transition-property: none;
7528
+ }
7529
+
7530
+ &[data-starting-style],
7531
+ &[data-ending-style] {
7532
+ pointer-events: none;
7533
+ filter: blur(8px);
7534
+ }
7535
+
7536
+ &[data-starting-style][data-direction="forward"],
7537
+ &[data-ending-style][data-direction="back"] {
7538
+ translate: 100% 0;
7539
+ }
7540
+
7541
+ &[data-ending-style][data-direction="forward"],
7542
+ &[data-starting-style][data-direction="back"] {
7543
+ translate: -100% 0;
7544
+ }
7545
+ }
5826
7546
  }
5827
7547
 
5828
- &:focus-visible {
5829
- outline-color: currentColor;
5830
- outline-offset: 2px;
7548
+ & .media-menu__separator {
7549
+ margin: 0.25rem 0;
7550
+ border-bottom: 1px solid oklch(1 0 0 / 0.1);
5831
7551
  }
5832
7552
 
5833
- &[aria-disabled="true"] {
5834
- pointer-events: none;
5835
- cursor: not-allowed;
5836
- opacity: 0.5;
7553
+ & .media-menu__group {
7554
+ display: flex;
7555
+ flex-direction: column;
7556
+ gap: 0.125rem;
7557
+
7558
+ @supports (top: anchor(top)) {
7559
+ &::before {
7560
+ position: absolute;
7561
+ position-anchor: --media-menu-item-highlight-anchor;
7562
+ inset: anchor(inside);
7563
+ pointer-events: none;
7564
+ content: "";
7565
+ background-color: oklch(from currentColor l c h / 0.1);
7566
+ border-radius: var(--menu-item-border-radius);
7567
+ transition: inset ease-in-out 100ms;
7568
+ }
7569
+ }
5837
7570
  }
5838
- }
5839
7571
 
5840
- .media-minimal-skin .media-popover.media-menu .media-menu__indicator {
5841
- flex-shrink: 0;
5842
- margin-right: -0.25rem;
5843
- opacity: 0;
5844
- }
7572
+ & .media-menu__item,
7573
+ & .media-menu__back {
7574
+ display: flex;
7575
+ gap: 0.375rem;
7576
+ align-items: center;
7577
+ padding: 0.375rem 0.5rem;
7578
+ text-align: left;
7579
+ text-shadow: 0 1px 0 var(--media-current-shadow-color);
7580
+ cursor: pointer;
7581
+ user-select: none;
7582
+ outline: 2px solid transparent;
7583
+ outline-offset: -2px;
7584
+ border-radius: var(--menu-item-border-radius);
7585
+ transition: background-color, color;
7586
+ transition-timing-function: ease-out;
7587
+ transition-duration: 100ms;
5845
7588
 
5846
- .media-minimal-skin .media-popover.media-menu .media-menu__item[aria-checked="true"] .media-menu__indicator {
5847
- opacity: 1;
7589
+ &:focus-visible {
7590
+ outline-color: currentColor;
7591
+ outline-offset: 2px;
7592
+ }
7593
+
7594
+ &:hover,
7595
+ &[data-highlighted] {
7596
+ background-color: oklch(from currentColor l c h / 0.1);
7597
+
7598
+ @supports (top: anchor(top)) {
7599
+ anchor-name: --media-menu-item-highlight-anchor;
7600
+ background-color: transparent;
7601
+ }
7602
+ }
7603
+
7604
+ & .media-icon {
7605
+ flex-shrink: 0;
7606
+ color: oklch(from currentColor l c h / 0.5);
7607
+ filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
7608
+ }
7609
+ }
7610
+
7611
+ & .media-menu__indicator {
7612
+ flex-shrink: 0;
7613
+ margin-right: -0.25rem;
7614
+ margin-left: auto;
7615
+ opacity: 0;
7616
+ }
7617
+
7618
+ & .media-menu__item {
7619
+ justify-content: space-between;
7620
+ font-variant-numeric: tabular-nums;
7621
+ color: inherit;
7622
+
7623
+ &[aria-disabled="true"] {
7624
+ pointer-events: none;
7625
+ cursor: not-allowed;
7626
+ opacity: 0.5;
7627
+ }
7628
+
7629
+ &[aria-checked="true"] .media-menu__indicator {
7630
+ opacity: 1;
7631
+ }
7632
+
7633
+ &[data-availability="unavailable"],
7634
+ &[data-availability="unsupported"] {
7635
+ display: none;
7636
+ }
7637
+ }
7638
+
7639
+ & .media-menu__tier {
7640
+ padding-top: 1px;
7641
+ padding-left: 0.125rem;
7642
+ font-size: 0.5625rem;
7643
+ font-weight: 600;
7644
+ line-height: 1;
7645
+ color: oklch(from currentColor l c h / 0.7);
7646
+ }
7647
+
7648
+ & .media-menu__back {
7649
+ width: 100%;
7650
+ margin-bottom: 0.125rem;
7651
+ }
7652
+
7653
+ & .media-menu__hint {
7654
+ display: inline-flex;
7655
+ gap: 0.25rem;
7656
+ align-items: center;
7657
+ min-width: 0;
7658
+ padding-left: 0.5rem;
7659
+ margin-left: auto;
7660
+ font-size: 0.75rem;
7661
+ color: oklch(from currentColor l c h / 0.65);
7662
+ }
7663
+
7664
+ & .media-menu__hint-label {
7665
+ max-width: 6rem;
7666
+ overflow: hidden;
7667
+ text-overflow: ellipsis;
7668
+ white-space: nowrap;
7669
+ }
7670
+
7671
+ & .media-menu__chevron {
7672
+ width: 0.875rem;
7673
+ height: 0.875rem;
7674
+ }
7675
+
7676
+ /* Settings menu */
7677
+ &.media-menu--settings {
7678
+ width: var(--media-menu-width);
7679
+ min-width: 12rem;
7680
+ height: var(--media-menu-height);
7681
+ overflow: hidden;
7682
+ /* Add height and width transitions. */
7683
+ transition:
7684
+ var(--media-popup-transition),
7685
+ height var(--media-popup-transition-timing-function) var(--menu-transition-duration),
7686
+ width var(--media-popup-transition-timing-function) var(--menu-transition-duration);
7687
+
7688
+ /* Don't transition width and height on open/close. */
7689
+ &[data-starting-style],
7690
+ &[data-ending-style] {
7691
+ transition: var(--media-popup-transition);
7692
+ }
7693
+ }
5848
7694
  }
5849
7695
 
5850
7696
  /* ==========================================================================
@@ -5871,10 +7717,11 @@ function VolumePopover(): ReactNode {
5871
7717
 
5872
7718
  /* Play: ended → restart */
5873
7719
  .media-button--play[data-ended] .media-icon--restart,
5874
- /* Play: paused (not ended) → play */
7720
+ /* Play: paused or not yet started (not ended) → play */
5875
7721
  .media-button--play:not([data-ended])[data-paused] .media-icon--play,
5876
- /* Play: playing (not paused, not ended) → pause */
5877
- .media-button--play:not([data-paused]):not([data-ended]) .media-icon--pause,
7722
+ .media-button--play:not([data-ended]):not([data-started]) .media-icon--play,
7723
+ /* Play: started and not paused/ended pause */
7724
+ .media-button--play[data-started]:not([data-paused]):not([data-ended]) .media-icon--pause,
5878
7725
  /* Mute: muted → volume off */
5879
7726
  .media-button--mute[data-muted] .media-icon--volume-off,
5880
7727
  /* Mute: volume low (not muted) → volume low */
@@ -5893,20 +7740,23 @@ function VolumePopover(): ReactNode {
5893
7740
  .media-minimal-skin--audio {
5894
7741
  --media-controls-background-color: oklch(1 0 0);
5895
7742
  --media-controls-backdrop-filter: blur(16px) saturate(1.5);
5896
- --media-controls-border-color: oklch(0 0 0 / 0.05);
7743
+ --media-controls-border-color: oklch(0 0 0 / 0.1);
5897
7744
  --media-controls-text-color: var(--media-color-primary, oklch(0 0 0));
5898
7745
  --media-error-dialog-transition-duration: 250ms;
5899
7746
  --media-error-dialog-transition-delay: 100ms;
5900
7747
  --media-popup-transition-duration: 100ms;
5901
7748
  --media-popup-transition-timing-function: ease-out;
5902
- --media-tooltip-background-color: oklch(1 0 0 / 0.1);
5903
- --media-tooltip-border-color: oklch(0 0 0 / 0.05);
7749
+ --media-tooltip-background-color: oklch(1 0 0);
7750
+ --media-tooltip-border-color: oklch(0 0 0 / 0.1);
5904
7751
  --media-tooltip-backdrop-filter: blur(16px) saturate(1.5);
5905
7752
  --media-tooltip-text-color: currentColor;
5906
7753
  --media-tooltip-side-offset: 0.75rem;
5907
7754
  --media-tooltip-boundary-offset: 0.75rem;
5908
- --media-popover-side-offset: 0.75rem;
5909
- --media-popover-boundary-offset: 0.75rem;
7755
+ --media-popover-background-color: var(--media-tooltip-background-color);
7756
+ --media-popover-border-color: var(--media-tooltip-border-color);
7757
+ --media-popover-backdrop-filter: var(--media-tooltip-backdrop-filter);
7758
+ --media-popover-side-offset: var(--media-tooltip-side-offset);
7759
+ --media-popover-boundary-offset: var(--media-tooltip-boundary-offset);
5910
7760
 
5911
7761
  @media (prefers-reduced-motion: reduce) {
5912
7762
  --media-error-dialog-transition-duration: 50ms;
@@ -5978,6 +7828,26 @@ function VolumePopover(): ReactNode {
5978
7828
  box-shadow: 0 0 0 1px var(--media-controls-border-color);
5979
7829
  }
5980
7830
 
7831
+ /* ==========================================================================
7832
+ Buffering (spinner over play button — overlay only, no layout change)
7833
+ ========================================================================== */
7834
+
7835
+ .media-minimal-skin--audio .media-button--play__wrapper {
7836
+ position: relative;
7837
+ display: inline-flex;
7838
+ }
7839
+
7840
+ .media-minimal-skin--audio .media-button--play__wrapper .media-buffering-indicator {
7841
+ color: inherit;
7842
+ }
7843
+
7844
+ .media-minimal-skin--audio
7845
+ .media-button--play__wrapper:has(.media-buffering-indicator[data-visible])
7846
+ .media-button--play
7847
+ .media-icon {
7848
+ opacity: 0;
7849
+ }
7850
+
5981
7851
  /* ==========================================================================
5982
7852
  Popups & Animations
5983
7853
  ========================================================================== */
@@ -5987,4 +7857,8 @@ function VolumePopover(): ReactNode {
5987
7857
  background: linear-gradient(to left, var(--media-controls-background-color) 80%, transparent 100%);
5988
7858
  }
5989
7859
 
7860
+ .media-minimal-skin--audio .media-slider__preview .media-slider__value {
7861
+ bottom: 2.5rem;
7862
+ }
7863
+
5990
7864
  ```