{ "name": "media-player", "type": "registry:ui", "files": [ { "path": "ui/media-player.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { Slider as SliderPrimitive } from \"@base-ui/react/slider\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport {\n AlertTriangleIcon,\n CaptionsOffIcon,\n CheckIcon,\n DownloadIcon,\n FastForwardIcon,\n Loader2Icon,\n Maximize2Icon,\n Minimize2Icon,\n PauseIcon,\n PictureInPicture2Icon,\n PictureInPictureIcon,\n PlayIcon,\n RefreshCcwIcon,\n RepeatIcon,\n RewindIcon,\n RotateCcwIcon,\n SettingsIcon,\n SubtitlesIcon,\n Volume1Icon,\n Volume2Icon,\n VolumeXIcon,\n} from \"lucide-react\";\nimport {\n MediaActionTypes,\n MediaProvider,\n timeUtils,\n useMediaDispatch,\n useMediaFullscreenRef,\n useMediaRef,\n useMediaSelector,\n} from \"media-chrome/react/media-store\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { useLazyRef } from \"@/registry/bases/base/hooks/use-lazy-ref\";\nimport { useComposedRefs } from \"@/registry/bases/base/lib/compose-refs\";\nimport { Badge } from \"@/registry/bases/base/ui/badge\";\nimport { Button } from \"@/registry/bases/base/ui/button\";\nimport { useDirection } from \"@/registry/bases/base/ui/direction\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"@/registry/bases/base/ui/dropdown-menu\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/registry/bases/base/ui/tooltip\";\n\nconst ROOT_NAME = \"MediaPlayer\";\nconst SEEK_NAME = \"MediaPlayerSeek\";\nconst SETTINGS_NAME = \"MediaPlayerSettings\";\nconst VOLUME_NAME = \"MediaPlayerVolume\";\nconst PLAYBACK_SPEED_NAME = \"MediaPlayerPlaybackSpeed\";\n\nconst FLOATING_MENU_SIDE_OFFSET = 10;\nconst SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];\n\ntype ButtonClickHandler = NonNullable<\n React.ComponentProps[\"onClick\"]\n>;\n\nconst SEEK_STEP_SHORT = 5;\nconst SEEK_STEP_LONG = 10;\nconst SEEK_COLLISION_PADDING = 10;\nconst SEEK_TOOLTIP_WIDTH_FALLBACK = 240;\n\nconst SEEK_HOVER_PERCENT = \"--seek-hover-percent\";\nconst SEEK_TOOLTIP_X = \"--seek-tooltip-x\";\nconst SEEK_TOOLTIP_Y = \"--seek-tooltip-y\";\n\nconst SPRITE_CONTAINER_WIDTH = 224;\nconst SPRITE_CONTAINER_HEIGHT = 128;\n\ninterface DivProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {}\n\ntype RootElement = HTMLDivElement;\n\ntype Direction = \"ltr\" | \"rtl\";\n\ninterface StoreState {\n controlsVisible: boolean;\n dragging: boolean;\n menuOpen: boolean;\n volumeIndicatorVisible: boolean;\n}\n\ninterface Store {\n subscribe: (cb: () => void) => () => void;\n getState: () => StoreState;\n setState: (\n key: keyof StoreState,\n value: StoreState[keyof StoreState],\n ) => void;\n notify: () => void;\n}\n\nconst StoreContext = React.createContext(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\nfunction useStore(selector: (state: StoreState) => T): T {\n const store = useStoreContext(\"useStore\");\n\n const getSnapshot = React.useCallback(\n () => selector(store.getState()),\n [store, selector],\n );\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface MediaPlayerContextValue {\n mediaId: string;\n labelId: string;\n descriptionId: string;\n dir: Direction;\n rootRef: React.RefObject;\n mediaRef: React.RefObject;\n portalContainer: HTMLElement | ShadowRoot | null;\n tooltipDelay: number;\n tooltipSideOffset: number;\n disabled: boolean;\n isVideo: boolean;\n withoutTooltip: boolean;\n}\n\nconst MediaPlayerContext = React.createContext(\n null,\n);\n\nfunction useMediaPlayerContext(consumerName: string) {\n const context = React.useContext(MediaPlayerContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface MediaPlayerProps\n extends Omit {\n onPlay?: () => void;\n onPause?: () => void;\n onEnded?: () => void;\n onTimeUpdate?: (time: number) => void;\n onVolumeChange?: (volume: number) => void;\n onMuted?: (muted: boolean) => void;\n onMediaError?: (error: MediaError | null) => void;\n onPipError?: (error: unknown, state: \"enter\" | \"exit\") => void;\n onFullscreenChange?: (fullscreen: boolean) => void;\n dir?: Direction;\n label?: string;\n tooltipDelay?: number;\n tooltipSideOffset?: number;\n autoHide?: boolean;\n disabled?: boolean;\n withoutTooltip?: boolean;\n}\n\nfunction MediaPlayer(props: MediaPlayerProps) {\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef(() => ({\n controlsVisible: true,\n dragging: false,\n menuOpen: false,\n volumeIndicatorVisible: false,\n }));\n\n const store: Store = React.useMemo(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n setState: (key, value) => {\n if (Object.is(stateRef.current[key], value)) return;\n stateRef.current[key] = value;\n store.notify();\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n },\n };\n }, [listenersRef, stateRef]);\n\n return (\n \n \n \n \n \n );\n}\n\nfunction MediaPlayerImpl(props: MediaPlayerProps) {\n const {\n onPlay,\n onPause,\n onEnded,\n onTimeUpdate,\n onFullscreenChange,\n onVolumeChange,\n onMuted,\n onMediaError,\n onPipError,\n dir: dirProp,\n label,\n tooltipDelay = 600,\n tooltipSideOffset = FLOATING_MENU_SIDE_OFFSET,\n render,\n autoHide = false,\n disabled = false,\n withoutTooltip = false,\n children,\n className,\n ref,\n ...rootImplProps\n } = props;\n\n const mediaId = React.useId();\n const labelId = React.useId();\n const descriptionId = React.useId();\n\n const rootRef = React.useRef(null);\n const fullscreenRef = useMediaFullscreenRef();\n const composedRef = useComposedRefs(ref, rootRef, fullscreenRef);\n\n const contextDir = useDirection();\n const dir = dirProp ?? contextDir;\n const dispatch = useMediaDispatch();\n const mediaRef = React.useRef(\n null,\n );\n\n const store = useStoreContext(ROOT_NAME);\n\n const controlsVisible = useStore((state) => state.controlsVisible);\n const dragging = useStore((state) => state.dragging);\n const menuOpen = useStore((state) => state.menuOpen);\n\n const hideControlsTimeoutRef = React.useRef(null);\n const lastMouseMoveRef = React.useRef(Date.now());\n const volumeIndicatorTimeoutRef = React.useRef(null);\n\n const mediaPaused = useMediaSelector((state) => state.mediaPaused ?? true);\n const isFullscreen = useMediaSelector(\n (state) => state.mediaIsFullscreen ?? false,\n );\n\n const [mounted, setMounted] = React.useState(false);\n React.useLayoutEffect(() => {\n setMounted(true);\n }, []);\n\n const portalContainer = mounted\n ? isFullscreen\n ? rootRef.current\n : globalThis.document.body\n : null;\n\n const isVideo =\n (typeof HTMLVideoElement !== \"undefined\" &&\n mediaRef.current instanceof HTMLVideoElement) ||\n mediaRef.current?.tagName?.toLowerCase() === \"mux-player\";\n\n const onControlsShow = React.useCallback(() => {\n store.setState(\"controlsVisible\", true);\n lastMouseMoveRef.current = Date.now();\n\n if (hideControlsTimeoutRef.current) {\n clearTimeout(hideControlsTimeoutRef.current);\n }\n\n if (autoHide && !mediaPaused && !menuOpen && !dragging) {\n hideControlsTimeoutRef.current = setTimeout(() => {\n store.setState(\"controlsVisible\", false);\n }, 3000);\n }\n }, [store.setState, autoHide, mediaPaused, menuOpen, dragging]);\n\n const onVolumeIndicatorTrigger = React.useCallback(() => {\n if (menuOpen) return;\n\n store.setState(\"volumeIndicatorVisible\", true);\n\n if (volumeIndicatorTimeoutRef.current) {\n clearTimeout(volumeIndicatorTimeoutRef.current);\n }\n\n volumeIndicatorTimeoutRef.current = setTimeout(() => {\n store.setState(\"volumeIndicatorVisible\", false);\n }, 2000);\n\n if (autoHide) {\n onControlsShow();\n }\n }, [store.setState, menuOpen, autoHide, onControlsShow]);\n\n const onMouseLeave = React.useCallback(\n (event: React.MouseEvent) => {\n rootImplProps.onMouseLeave?.(event);\n\n if (event.defaultPrevented) return;\n\n if (autoHide && !mediaPaused && !menuOpen && !dragging) {\n store.setState(\"controlsVisible\", false);\n }\n },\n [\n store.setState,\n rootImplProps.onMouseLeave,\n autoHide,\n mediaPaused,\n menuOpen,\n dragging,\n ],\n );\n\n const onMouseMove = React.useCallback(\n (event: React.MouseEvent) => {\n rootImplProps.onMouseMove?.(event);\n\n if (event.defaultPrevented) return;\n\n if (autoHide) {\n onControlsShow();\n }\n },\n [autoHide, rootImplProps.onMouseMove, onControlsShow],\n );\n\n React.useEffect(() => {\n if (mediaPaused || menuOpen || dragging) {\n store.setState(\"controlsVisible\", true);\n if (hideControlsTimeoutRef.current) {\n clearTimeout(hideControlsTimeoutRef.current);\n }\n return;\n }\n\n if (autoHide) {\n onControlsShow();\n }\n }, [\n store.setState,\n onControlsShow,\n autoHide,\n menuOpen,\n mediaPaused,\n dragging,\n ]);\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (disabled) return;\n\n rootImplProps.onKeyDown?.(event);\n\n if (event.defaultPrevented) return;\n\n const mediaElement = mediaRef.current;\n if (!mediaElement) return;\n\n const isMediaFocused = document.activeElement === mediaElement;\n const isPlayerFocused =\n document.activeElement?.closest('[data-slot=\"media-player\"]') !== null;\n\n if (!isMediaFocused && !isPlayerFocused) return;\n\n if (autoHide) onControlsShow();\n\n switch (event.key.toLowerCase()) {\n case \" \":\n case \"k\":\n event.preventDefault();\n dispatch({\n type: mediaElement.paused\n ? MediaActionTypes.MEDIA_PLAY_REQUEST\n : MediaActionTypes.MEDIA_PAUSE_REQUEST,\n });\n break;\n\n case \"f\":\n event.preventDefault();\n dispatch({\n type: document.fullscreenElement\n ? MediaActionTypes.MEDIA_EXIT_FULLSCREEN_REQUEST\n : MediaActionTypes.MEDIA_ENTER_FULLSCREEN_REQUEST,\n });\n break;\n\n case \"m\": {\n event.preventDefault();\n if (isVideo) {\n onVolumeIndicatorTrigger();\n }\n dispatch({\n type: mediaElement.muted\n ? MediaActionTypes.MEDIA_UNMUTE_REQUEST\n : MediaActionTypes.MEDIA_MUTE_REQUEST,\n });\n break;\n }\n\n case \"arrowright\":\n event.preventDefault();\n if (\n isVideo ||\n (mediaElement instanceof HTMLAudioElement && event.shiftKey)\n ) {\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: Math.min(\n mediaElement.duration,\n mediaElement.currentTime + SEEK_STEP_SHORT,\n ),\n });\n }\n break;\n\n case \"arrowleft\":\n event.preventDefault();\n if (\n isVideo ||\n (mediaElement instanceof HTMLAudioElement && event.shiftKey)\n ) {\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: Math.max(0, mediaElement.currentTime - SEEK_STEP_SHORT),\n });\n }\n break;\n\n case \"arrowup\":\n event.preventDefault();\n if (isVideo) {\n onVolumeIndicatorTrigger();\n dispatch({\n type: MediaActionTypes.MEDIA_VOLUME_REQUEST,\n detail: Math.min(1, mediaElement.volume + 0.1),\n });\n }\n break;\n\n case \"arrowdown\":\n event.preventDefault();\n if (isVideo) {\n onVolumeIndicatorTrigger();\n dispatch({\n type: MediaActionTypes.MEDIA_VOLUME_REQUEST,\n detail: Math.max(0, mediaElement.volume - 0.1),\n });\n }\n break;\n\n case \"<\": {\n event.preventDefault();\n const currentRate = mediaElement.playbackRate;\n const currentIndex = SPEEDS.indexOf(currentRate);\n const newIndex = Math.max(0, currentIndex - 1);\n const newRate = SPEEDS[newIndex] ?? 1;\n dispatch({\n type: MediaActionTypes.MEDIA_PLAYBACK_RATE_REQUEST,\n detail: newRate,\n });\n break;\n }\n\n case \">\": {\n event.preventDefault();\n const currentRate = mediaElement.playbackRate;\n const currentIndex = SPEEDS.indexOf(currentRate);\n const newIndex = Math.min(SPEEDS.length - 1, currentIndex + 1);\n const newRate = SPEEDS[newIndex] ?? 1;\n dispatch({\n type: MediaActionTypes.MEDIA_PLAYBACK_RATE_REQUEST,\n detail: newRate,\n });\n break;\n }\n\n case \"c\":\n event.preventDefault();\n if (isVideo && mediaElement.textTracks.length > 0) {\n dispatch({\n type: MediaActionTypes.MEDIA_TOGGLE_SUBTITLES_REQUEST,\n });\n }\n break;\n\n case \"d\": {\n const hasDownload = mediaElement.querySelector(\n '[data-slot=\"media-player-download\"]',\n );\n\n if (!hasDownload) break;\n\n event.preventDefault();\n if (mediaElement.currentSrc) {\n const link = document.createElement(\"a\");\n link.href = mediaElement.currentSrc;\n link.download = \"\";\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n }\n break;\n }\n\n case \"p\": {\n event.preventDefault();\n if (isVideo && \"requestPictureInPicture\" in mediaElement) {\n const isPip = document.pictureInPictureElement === mediaElement;\n dispatch({\n type: isPip\n ? MediaActionTypes.MEDIA_EXIT_PIP_REQUEST\n : MediaActionTypes.MEDIA_ENTER_PIP_REQUEST,\n });\n if (isPip) {\n document.exitPictureInPicture().catch((error) => {\n onPipError?.(error, \"exit\");\n });\n } else {\n mediaElement.requestPictureInPicture().catch((error) => {\n onPipError?.(error, \"enter\");\n });\n }\n }\n break;\n }\n\n case \"r\": {\n event.preventDefault();\n mediaElement.loop = !mediaElement.loop;\n break;\n }\n\n case \"j\": {\n event.preventDefault();\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: Math.max(0, mediaElement.currentTime - SEEK_STEP_LONG),\n });\n break;\n }\n\n case \"l\": {\n event.preventDefault();\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: Math.min(\n mediaElement.duration,\n mediaElement.currentTime + SEEK_STEP_LONG,\n ),\n });\n break;\n }\n\n case \"0\":\n case \"1\":\n case \"2\":\n case \"3\":\n case \"4\":\n case \"5\":\n case \"6\":\n case \"7\":\n case \"8\":\n case \"9\": {\n event.preventDefault();\n const percent = Number.parseInt(event.key, 10) / 10;\n const seekTime = mediaElement.duration * percent;\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: seekTime,\n });\n break;\n }\n\n case \"home\": {\n event.preventDefault();\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: 0,\n });\n break;\n }\n\n case \"end\": {\n event.preventDefault();\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: mediaElement.duration,\n });\n break;\n }\n }\n },\n [\n dispatch,\n rootImplProps.onKeyDown,\n onVolumeIndicatorTrigger,\n onPipError,\n disabled,\n isVideo,\n onControlsShow,\n autoHide,\n ],\n );\n\n const onKeyUp = React.useCallback(\n (event: React.KeyboardEvent) => {\n rootImplProps.onKeyUp?.(event);\n\n const key = event.key.toLowerCase();\n if (key === \"arrowup\" || key === \"arrowdown\" || key === \"m\") {\n onVolumeIndicatorTrigger();\n }\n },\n [rootImplProps.onKeyUp, onVolumeIndicatorTrigger],\n );\n\n React.useEffect(() => {\n const mediaElement = mediaRef.current;\n if (!mediaElement) return;\n\n if (onPlay) mediaElement.addEventListener(\"play\", onPlay);\n if (onPause) mediaElement.addEventListener(\"pause\", onPause);\n if (onEnded) mediaElement.addEventListener(\"ended\", onEnded);\n if (onTimeUpdate)\n mediaElement.addEventListener(\"timeupdate\", () =>\n onTimeUpdate?.(mediaElement.currentTime),\n );\n if (onVolumeChange)\n mediaElement.addEventListener(\"volumechange\", () => {\n onVolumeChange?.(mediaElement.volume);\n onMuted?.(mediaElement.muted);\n });\n if (onMediaError)\n mediaElement.addEventListener(\"error\", () =>\n onMediaError?.(mediaElement.error),\n );\n if (onFullscreenChange) {\n document.addEventListener(\"fullscreenchange\", () =>\n onFullscreenChange?.(!!document.fullscreenElement),\n );\n }\n\n return () => {\n if (onPlay) mediaElement.removeEventListener(\"play\", onPlay);\n if (onPause) mediaElement.removeEventListener(\"pause\", onPause);\n if (onEnded) mediaElement.removeEventListener(\"ended\", onEnded);\n if (onTimeUpdate)\n mediaElement.removeEventListener(\"timeupdate\", () =>\n onTimeUpdate?.(mediaElement.currentTime),\n );\n if (onVolumeChange)\n mediaElement.removeEventListener(\"volumechange\", () => {\n onVolumeChange?.(mediaElement.volume);\n onMuted?.(mediaElement.muted);\n });\n if (onMediaError)\n mediaElement.removeEventListener(\"error\", () =>\n onMediaError?.(mediaElement.error),\n );\n if (onFullscreenChange) {\n document.removeEventListener(\"fullscreenchange\", () =>\n onFullscreenChange?.(!!document.fullscreenElement),\n );\n }\n if (volumeIndicatorTimeoutRef.current) {\n clearTimeout(volumeIndicatorTimeoutRef.current);\n }\n if (hideControlsTimeoutRef.current) {\n clearTimeout(hideControlsTimeoutRef.current);\n }\n };\n }, [\n onPlay,\n onPause,\n onEnded,\n onTimeUpdate,\n onVolumeChange,\n onMuted,\n onMediaError,\n onFullscreenChange,\n ]);\n\n const contextValue = React.useMemo(\n () => ({\n mediaId,\n labelId,\n descriptionId,\n dir,\n rootRef,\n mediaRef,\n portalContainer,\n tooltipDelay,\n tooltipSideOffset,\n disabled,\n isVideo,\n withoutTooltip,\n }),\n [\n mediaId,\n labelId,\n descriptionId,\n dir,\n portalContainer,\n tooltipDelay,\n tooltipSideOffset,\n disabled,\n isVideo,\n withoutTooltip,\n ],\n );\n\n return (\n \n {useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n \"aria-labelledby\": labelId,\n \"aria-describedby\": descriptionId,\n \"aria-disabled\": disabled,\n dir,\n tabIndex: disabled ? undefined : 0,\n ref: composedRef,\n onMouseLeave,\n onMouseMove,\n onKeyDown,\n onKeyUp,\n className: cn(\n \"dark relative isolate flex size-full flex-col overflow-hidden rounded-lg bg-background outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_video]:relative [&_video]:object-contain\",\n \"in-[:fullscreen]:flex in-[:fullscreen]:h-full in-[:fullscreen]:max-h-screen in-[:fullscreen]:flex-col in-[:fullscreen]:justify-between data-[state=fullscreen]:[&_video]:size-full\",\n \"**:data-slider:relative [&_[data-slider]::before]:absolute [&_[data-slider]::before]:inset-x-0 [&_[data-slider]::before]:-top-4 [&_[data-slider]::before]:-bottom-2 [&_[data-slider]::before]:z-10 [&_[data-slider]::before]:h-8 [&_[data-slider]::before]:cursor-pointer [&_[data-slider]::before]:content-[''] [&_[data-slot='media-player-seek']:not([data-hovering])::before]:cursor-default\",\n \"[&_video::-webkit-media-text-track-display]:top-auto! [&_video::-webkit-media-text-track-display]:bottom-[4%]! [&_video::-webkit-media-text-track-display]:mb-0! data-[state=fullscreen]:data-controls-visible:[&_video::-webkit-media-text-track-display]:bottom-[9%]! data-[state=fullscreen]:[&_video::-webkit-media-text-track-display]:bottom-[7%]! data-controls-visible:[&_video::-webkit-media-text-track-display]:bottom-[13%]!\",\n className,\n ),\n children: (\n <>\n \n {label ?? \"Media player\"}\n \n \n {isVideo\n ? \"Video player with custom controls for playback, volume, seeking, and more. Use space bar to play/pause, arrow keys (←/→) to seek, and arrow keys (↑/↓) to adjust volume.\"\n : \"Audio player with custom controls for playback, volume, seeking, and more. Use space bar to play/pause, Shift + arrow keys (←/→) to seek, and arrow keys (↑/↓) to adjust volume.\"}\n \n {children}\n \n \n ),\n },\n rootImplProps,\n ),\n render,\n state: {\n slot: \"media-player\",\n state: isFullscreen ? \"fullscreen\" : \"windowed\",\n disabled: disabled || undefined,\n \"controls-visible\": controlsVisible || undefined,\n },\n })}\n \n );\n}\n\ninterface MediaPlayerVideoProps\n extends React.ComponentProps<\"video\">,\n useRender.ComponentProps<\"video\"> {}\n\nfunction MediaPlayerVideo(props: MediaPlayerVideoProps) {\n const { render, ref, ...videoProps } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerVideo\");\n const dispatch = useMediaDispatch();\n const mediaRefCallback = useMediaRef();\n const composedRef = useComposedRefs(ref, context.mediaRef, mediaRefCallback);\n\n const onPlayToggle = React.useCallback(\n (event: React.MouseEvent) => {\n props.onClick?.(event);\n\n if (event.defaultPrevented) return;\n\n const mediaElement = event.currentTarget;\n if (!mediaElement) return;\n\n dispatch({\n type: mediaElement.paused\n ? MediaActionTypes.MEDIA_PLAY_REQUEST\n : MediaActionTypes.MEDIA_PAUSE_REQUEST,\n });\n },\n [dispatch, props.onClick],\n );\n\n return useRender({\n defaultTagName: \"video\",\n props: mergeProps<\"video\">(\n {\n \"aria-describedby\": context.descriptionId,\n \"aria-labelledby\": context.labelId,\n id: context.mediaId,\n ref: composedRef,\n onClick: onPlayToggle,\n },\n videoProps,\n ),\n render,\n state: { slot: \"media-player-video\" },\n });\n}\n\ninterface MediaPlayerAudioProps\n extends React.ComponentProps<\"audio\">,\n useRender.ComponentProps<\"audio\"> {}\n\nfunction MediaPlayerAudio(props: MediaPlayerAudioProps) {\n const { render, ref, ...audioProps } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerAudio\");\n const mediaRefCallback = useMediaRef();\n const composedRef = useComposedRefs(ref, context.mediaRef, mediaRefCallback);\n\n return useRender({\n defaultTagName: \"audio\",\n props: mergeProps<\"audio\">(\n {\n \"aria-describedby\": context.descriptionId,\n \"aria-labelledby\": context.labelId,\n id: context.mediaId,\n ref: composedRef,\n },\n audioProps,\n ),\n render,\n state: { slot: \"media-player-audio\" },\n });\n}\n\nfunction MediaPlayerControls(props: DivProps) {\n const { render, className, ...controlsProps } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerControls\");\n const isFullscreen = useMediaSelector(\n (state) => state.mediaIsFullscreen ?? false,\n );\n const controlsVisible = useStore((state) => state.controlsVisible);\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n dir: context.dir,\n className: cn(\n \"dark pointer-events-none absolute right-0 bottom-0 left-0 z-50 flex items-center gap-2 in-[:fullscreen]:px-6 px-4 in-[:fullscreen]:py-4 py-3 opacity-0 transition-opacity duration-200 data-visible:pointer-events-auto data-visible:opacity-100\",\n className,\n ),\n },\n controlsProps,\n ),\n render,\n state: {\n slot: \"media-player-controls\",\n state: isFullscreen ? \"fullscreen\" : \"windowed\",\n disabled: context.disabled || undefined,\n visible: controlsVisible || undefined,\n },\n });\n}\n\ninterface MediaPlayerLoadingProps extends DivProps {\n delayMs?: number;\n}\n\nfunction MediaPlayerLoading(props: MediaPlayerLoadingProps) {\n const { delayMs = 500, render, className, children, ...loadingProps } = props;\n\n const isLoading = useMediaSelector((state) => state.mediaLoading ?? false);\n const isPaused = useMediaSelector((state) => state.mediaPaused ?? true);\n const hasPlayed = useMediaSelector((state) => state.mediaHasPlayed ?? false);\n\n const shouldShowLoading = isLoading && !isPaused;\n const shouldUseDelay = hasPlayed && shouldShowLoading;\n const loadingDelayMs = shouldUseDelay ? delayMs : 0;\n\n const [shouldRender, setShouldRender] = React.useState(false);\n const timeoutRef = React.useRef(null);\n\n React.useEffect(() => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n\n if (shouldShowLoading) {\n if (loadingDelayMs > 0) {\n timeoutRef.current = setTimeout(() => {\n setShouldRender(true);\n timeoutRef.current = null;\n }, loadingDelayMs);\n } else {\n setShouldRender(true);\n }\n } else {\n setShouldRender(false);\n }\n\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n };\n }, [shouldShowLoading, loadingDelayMs]);\n\n const rendered = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"status\",\n \"aria-live\": \"polite\" as const,\n className: cn(\n \"fade-in-0 zoom-in-95 pointer-events-none absolute inset-0 z-50 flex animate-in items-center justify-center duration-200\",\n className,\n ),\n children: children ?? (\n \n ),\n },\n loadingProps,\n ),\n render,\n state: { slot: \"media-player-loading\" },\n });\n\n if (!shouldRender) return null;\n\n return rendered;\n}\n\ninterface MediaPlayerErrorProps extends DivProps {\n error?: MediaError | null;\n label?: string;\n description?: string;\n onRetry?: () => void;\n onReload?: () => void;\n}\n\nfunction MediaPlayerError(props: MediaPlayerErrorProps) {\n const {\n error: errorProp,\n label,\n description,\n onRetry: onRetryProp,\n onReload: onReloadProp,\n render,\n className,\n children,\n ...errorProps\n } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerError\");\n const isFullscreen = useMediaSelector(\n (state) => state.mediaIsFullscreen ?? false,\n );\n const mediaError = useMediaSelector((state) => state.mediaError);\n\n const error = errorProp ?? mediaError;\n\n const labelId = React.useId();\n const descriptionId = React.useId();\n\n const [actionState, setActionState] = React.useState<{\n retryPending: boolean;\n reloadPending: boolean;\n }>({\n retryPending: false,\n reloadPending: false,\n });\n\n const onRetry = React.useCallback(() => {\n setActionState((prev) => ({ ...prev, retryPending: true }));\n\n requestAnimationFrame(() => {\n const mediaElement = context.mediaRef.current;\n if (!mediaElement) {\n setActionState((prev) => ({ ...prev, retryPending: false }));\n return;\n }\n\n if (onRetryProp) {\n onRetryProp();\n } else {\n const currentSrc = mediaElement.currentSrc ?? mediaElement.src;\n if (currentSrc) {\n mediaElement.load();\n }\n }\n\n setActionState((prev) => ({ ...prev, retryPending: false }));\n });\n }, [context.mediaRef, onRetryProp]);\n\n const onReload = React.useCallback(() => {\n setActionState((prev) => ({ ...prev, reloadPending: true }));\n\n requestAnimationFrame(() => {\n if (onReloadProp) {\n onReloadProp();\n } else {\n window.location.reload();\n }\n });\n }, [onReloadProp]);\n\n const errorLabel = React.useMemo(() => {\n if (label) return label;\n\n if (!error) return \"Playback Error\";\n\n const labelMap: Record = {\n [MediaError.MEDIA_ERR_ABORTED]: \"Playback Interrupted\",\n [MediaError.MEDIA_ERR_NETWORK]: \"Connection Problem\",\n [MediaError.MEDIA_ERR_DECODE]: \"Media Error\",\n [MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED]: \"Unsupported Format\",\n };\n\n return labelMap[error.code] ?? \"Playback Error\";\n }, [label, error]);\n\n const errorDescription = React.useMemo(() => {\n if (description) return description;\n\n if (!error) return \"An unknown error occurred\";\n\n const descriptionMap: Record = {\n [MediaError.MEDIA_ERR_ABORTED]: \"Media playback was aborted\",\n [MediaError.MEDIA_ERR_NETWORK]:\n \"A network error occurred while loading the media\",\n [MediaError.MEDIA_ERR_DECODE]:\n \"An error occurred while decoding the media\",\n [MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED]:\n \"The media format is not supported\",\n };\n\n return descriptionMap[error.code] ?? \"An unknown error occurred\";\n }, [description, error]);\n\n const rendered = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"alert\",\n \"aria-describedby\": descriptionId,\n \"aria-labelledby\": labelId,\n \"aria-live\": \"assertive\" as const,\n className: cn(\n \"pointer-events-auto absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/80 text-white backdrop-blur-sm\",\n className,\n ),\n children: children ?? (\n
\n \n
\n

\n {errorLabel}\n

\n

\n {errorDescription}\n

\n
\n
\n \n {actionState.retryPending ? (\n \n ) : (\n \n )}\n Try again\n \n \n {actionState.reloadPending ? (\n \n ) : (\n \n )}\n Reload page\n \n
\n
\n ),\n },\n errorProps,\n ),\n render,\n state: {\n slot: \"media-player-error\",\n state: isFullscreen ? \"fullscreen\" : \"windowed\",\n },\n });\n\n if (!error) return null;\n\n return rendered;\n}\n\nfunction MediaPlayerVolumeIndicator(props: DivProps) {\n const { render, className, ...indicatorProps } = props;\n\n const mediaVolume = useMediaSelector((state) => state.mediaVolume ?? 1);\n const mediaMuted = useMediaSelector((state) => state.mediaMuted ?? false);\n const mediaVolumeLevel = useMediaSelector(\n (state) => state.mediaVolumeLevel ?? \"high\",\n );\n const volumeIndicatorVisible = useStore(\n (state) => state.volumeIndicatorVisible,\n );\n\n const effectiveVolume = mediaMuted ? 0 : mediaVolume;\n const volumePercentage = Math.round(effectiveVolume * 100);\n const barCount = 10;\n const activeBarCount = Math.ceil(effectiveVolume * barCount);\n\n const rendered = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n role: \"status\",\n \"aria-live\": \"polite\" as const,\n \"aria-label\": `Volume ${mediaMuted ? \"muted\" : `${volumePercentage}%`}`,\n className: cn(\n \"pointer-events-none absolute inset-0 z-50 flex items-center justify-center\",\n className,\n ),\n children: (\n
\n
\n {mediaVolumeLevel === \"off\" || mediaMuted ? (\n \n ) : mediaVolumeLevel === \"high\" ? (\n \n ) : (\n \n )}\n \n {mediaMuted ? \"Muted\" : `${volumePercentage}%`}\n \n
\n
\n {Array.from({ length: barCount }, (_, index) => (\n \n ))}\n
\n
\n ),\n },\n indicatorProps,\n ),\n render,\n state: { slot: \"media-player-volume-indicator\" },\n });\n\n if (!volumeIndicatorVisible) return null;\n\n return rendered;\n}\n\nfunction MediaPlayerControlsOverlay(props: DivProps) {\n const { render, className, ...overlayProps } = props;\n\n const isFullscreen = useMediaSelector(\n (state) => state.mediaIsFullscreen ?? false,\n );\n const controlsVisible = useStore((state) => state.controlsVisible);\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"pointer-events-none absolute inset-0 -z-10 bg-linear-to-t from-black/80 to-transparent opacity-0 transition-opacity duration-200 data-visible:opacity-100\",\n className,\n ),\n },\n overlayProps,\n ),\n render,\n state: {\n slot: \"media-player-controls-overlay\",\n state: isFullscreen ? \"fullscreen\" : \"windowed\",\n visible: controlsVisible || undefined,\n },\n });\n}\n\nfunction MediaPlayerPlay(props: React.ComponentProps) {\n const { children, className, disabled, ...playButtonProps } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerPlay\");\n const dispatch = useMediaDispatch();\n const mediaPaused = useMediaSelector((state) => state.mediaPaused ?? true);\n\n const isDisabled = disabled || context.disabled;\n\n const onPlayToggle = React.useCallback(\n (event) => {\n props.onClick?.(event);\n\n if (event.defaultPrevented) return;\n\n dispatch({\n type: mediaPaused\n ? MediaActionTypes.MEDIA_PLAY_REQUEST\n : MediaActionTypes.MEDIA_PAUSE_REQUEST,\n });\n },\n [dispatch, props.onClick, mediaPaused],\n );\n\n return (\n \n \n {children ?? (mediaPaused ? : )}\n \n \n );\n}\n\ninterface MediaPlayerSeekBackwardProps\n extends React.ComponentProps {\n seconds?: number;\n}\n\nfunction MediaPlayerSeekBackward(props: MediaPlayerSeekBackwardProps) {\n const {\n seconds = SEEK_STEP_SHORT,\n children,\n className,\n disabled,\n ...seekBackwardProps\n } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerSeekBackward\");\n const dispatch = useMediaDispatch();\n const mediaCurrentTime = useMediaSelector(\n (state) => state.mediaCurrentTime ?? 0,\n );\n\n const isDisabled = disabled || context.disabled;\n\n const onSeekBackward = React.useCallback(\n (event) => {\n props.onClick?.(event);\n\n if (event.defaultPrevented) return;\n\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: Math.max(0, mediaCurrentTime - seconds),\n });\n },\n [dispatch, props.onClick, mediaCurrentTime, seconds],\n );\n\n return (\n \n \n {children ?? }\n \n \n );\n}\n\ninterface MediaPlayerSeekForwardProps\n extends React.ComponentProps {\n seconds?: number;\n}\n\nfunction MediaPlayerSeekForward(props: MediaPlayerSeekForwardProps) {\n const {\n seconds = SEEK_STEP_LONG,\n children,\n className,\n disabled,\n ...seekForwardProps\n } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerSeekForward\");\n const dispatch = useMediaDispatch();\n const mediaCurrentTime = useMediaSelector(\n (state) => state.mediaCurrentTime ?? 0,\n );\n const [, seekableEnd] = useMediaSelector(\n (state) => state.mediaSeekable ?? [0, 0],\n );\n const isDisabled = disabled || context.disabled;\n\n const onSeekForward = React.useCallback(\n (event) => {\n props.onClick?.(event);\n\n if (event.defaultPrevented) return;\n\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: Math.min(\n seekableEnd ?? Number.POSITIVE_INFINITY,\n mediaCurrentTime + seconds,\n ),\n });\n },\n [dispatch, props.onClick, mediaCurrentTime, seekableEnd, seconds],\n );\n\n return (\n \n \n {children ?? }\n \n \n );\n}\n\ninterface SeekState {\n isHovering: boolean;\n pendingSeekTime: number | null;\n hasInitialPosition: boolean;\n}\n\ninterface MediaPlayerSeekProps extends SliderPrimitive.Root.Props {\n withTime?: boolean;\n withoutChapter?: boolean;\n withoutTooltip?: boolean;\n tooltipThumbnailSrc?: string | ((time: number) => string);\n tooltipTimeVariant?: \"current\" | \"progress\";\n tooltipSideOffset?: number;\n tooltipCollisionBoundary?: Element | Element[];\n tooltipCollisionPadding?:\n | number\n | Partial>;\n}\n\nfunction MediaPlayerSeek(props: MediaPlayerSeekProps) {\n const {\n withTime = false,\n withoutChapter = false,\n withoutTooltip = false,\n tooltipTimeVariant = \"current\",\n tooltipThumbnailSrc,\n tooltipSideOffset,\n tooltipCollisionPadding = SEEK_COLLISION_PADDING,\n tooltipCollisionBoundary,\n className,\n disabled,\n ...seekProps\n } = props;\n\n const context = useMediaPlayerContext(SEEK_NAME);\n const store = useStoreContext(SEEK_NAME);\n const dispatch = useMediaDispatch();\n const mediaCurrentTime = useMediaSelector(\n (state) => state.mediaCurrentTime ?? 0,\n );\n const [seekableStart = 0, seekableEnd = 0] = useMediaSelector(\n (state) => state.mediaSeekable ?? [0, 0],\n );\n const mediaBuffered = useMediaSelector((state) => state.mediaBuffered ?? []);\n const mediaEnded = useMediaSelector((state) => state.mediaEnded ?? false);\n\n const chapterCues = useMediaSelector(\n (state) => state.mediaChaptersCues ?? [],\n );\n const mediaPreviewTime = useMediaSelector((state) => state.mediaPreviewTime);\n const mediaPreviewImage = useMediaSelector(\n (state) => state.mediaPreviewImage,\n );\n const mediaPreviewCoords = useMediaSelector(\n (state) => state.mediaPreviewCoords,\n );\n\n const seekRef = React.useRef(null);\n const tooltipRef = React.useRef(null);\n const justCommittedRef = React.useRef(false);\n\n const hoverTimeRef = React.useRef(0);\n const tooltipXRef = React.useRef(0);\n const tooltipYRef = React.useRef(0);\n const seekRectRef = React.useRef(null);\n const collisionDataRef = React.useRef<{\n padding: { top: number; right: number; bottom: number; left: number };\n boundaries: Element[];\n } | null>(null);\n\n const [seekState, setSeekState] = React.useState({\n isHovering: false,\n pendingSeekTime: null,\n hasInitialPosition: false,\n });\n\n const rafIdRef = React.useRef(null);\n const seekThrottleRef = React.useRef(null);\n const hoverTimeoutRef = React.useRef(null);\n const lastPointerXRef = React.useRef(0);\n const lastPointerYRef = React.useRef(0);\n const previewDebounceRef = React.useRef(null);\n const pointerEnterTimeRef = React.useRef(0);\n const horizontalMovementRef = React.useRef(0);\n const verticalMovementRef = React.useRef(0);\n const lastSeekCommitTimeRef = React.useRef(0);\n\n const timeCache = React.useRef>(new Map());\n\n const displayValue = seekState.pendingSeekTime ?? mediaCurrentTime;\n\n const isDisabled = disabled || context.disabled;\n const tooltipDisabled =\n withoutTooltip || context.withoutTooltip || store.getState().menuOpen;\n\n const currentTooltipSideOffset =\n tooltipSideOffset ?? context.tooltipSideOffset;\n\n const getCachedTime = React.useCallback((time: number, duration: number) => {\n const roundedTime = Math.floor(time);\n const key = roundedTime + duration * 10000;\n\n if (timeCache.current.has(key)) {\n return timeCache.current.get(key) as string;\n }\n\n const formatted = timeUtils.formatTime(time, duration);\n timeCache.current.set(key, formatted);\n\n if (timeCache.current.size > 100) {\n timeCache.current.clear();\n }\n\n return formatted;\n }, []);\n\n const currentTime = getCachedTime(displayValue, seekableEnd);\n const duration = getCachedTime(seekableEnd, seekableEnd);\n const remainingTime = getCachedTime(seekableEnd - displayValue, seekableEnd);\n\n const onCollisionDataUpdate = React.useCallback(() => {\n if (collisionDataRef.current) return collisionDataRef.current;\n\n const padding =\n typeof tooltipCollisionPadding === \"number\"\n ? {\n top: tooltipCollisionPadding,\n right: tooltipCollisionPadding,\n bottom: tooltipCollisionPadding,\n left: tooltipCollisionPadding,\n }\n : { top: 0, right: 0, bottom: 0, left: 0, ...tooltipCollisionPadding };\n\n const boundaries = tooltipCollisionBoundary\n ? Array.isArray(tooltipCollisionBoundary)\n ? tooltipCollisionBoundary\n : [tooltipCollisionBoundary]\n : ([context.rootRef.current].filter(Boolean) as Element[]);\n\n collisionDataRef.current = { padding, boundaries };\n return collisionDataRef.current;\n }, [tooltipCollisionPadding, tooltipCollisionBoundary, context.rootRef]);\n\n const getCurrentChapterCue = React.useCallback(\n (time: number) => {\n if (withoutChapter || chapterCues.length === 0) return null;\n return chapterCues.find((c) => time >= c.startTime && time < c.endTime);\n },\n [chapterCues, withoutChapter],\n );\n\n const getThumbnail = React.useCallback(\n (time: number) => {\n if (tooltipDisabled) return null;\n\n if (tooltipThumbnailSrc) {\n const src =\n typeof tooltipThumbnailSrc === \"function\"\n ? tooltipThumbnailSrc(time)\n : tooltipThumbnailSrc;\n return { src, coords: null };\n }\n\n if (\n mediaPreviewTime !== undefined &&\n Math.abs(time - mediaPreviewTime) < 0.1 &&\n mediaPreviewImage\n ) {\n return {\n src: mediaPreviewImage,\n coords: mediaPreviewCoords ?? null,\n };\n }\n\n return null;\n },\n [\n tooltipThumbnailSrc,\n mediaPreviewTime,\n mediaPreviewImage,\n mediaPreviewCoords,\n tooltipDisabled,\n ],\n );\n\n const onPreviewUpdate = React.useCallback(\n (time: number) => {\n if (tooltipDisabled) return;\n\n if (previewDebounceRef.current) {\n cancelAnimationFrame(previewDebounceRef.current);\n }\n\n previewDebounceRef.current = requestAnimationFrame(() => {\n dispatch({\n type: MediaActionTypes.MEDIA_PREVIEW_REQUEST,\n detail: time,\n });\n previewDebounceRef.current = null;\n });\n },\n [dispatch, tooltipDisabled],\n );\n\n const onTooltipPositionUpdate = React.useCallback(\n (clientX: number) => {\n if (!seekRef.current) return;\n\n const tooltipWidth =\n tooltipRef.current?.offsetWidth ?? SEEK_TOOLTIP_WIDTH_FALLBACK;\n\n let x = clientX;\n const y = seekRectRef.current?.top ?? 0;\n\n const collisionData = onCollisionDataUpdate();\n const halfTooltipWidth = tooltipWidth / 2;\n\n let minLeft = 0;\n let maxRight = window.innerWidth;\n\n for (const boundary of collisionData.boundaries) {\n const boundaryRect = boundary.getBoundingClientRect();\n minLeft = Math.max(\n minLeft,\n boundaryRect.left + collisionData.padding.left,\n );\n maxRight = Math.min(\n maxRight,\n boundaryRect.right - collisionData.padding.right,\n );\n }\n\n if (x - halfTooltipWidth < minLeft) {\n x = minLeft + halfTooltipWidth;\n } else if (x + halfTooltipWidth > maxRight) {\n x = maxRight - halfTooltipWidth;\n }\n\n const viewportPadding = SEEK_COLLISION_PADDING;\n if (x - halfTooltipWidth < viewportPadding) {\n x = viewportPadding + halfTooltipWidth;\n } else if (x + halfTooltipWidth > window.innerWidth - viewportPadding) {\n x = window.innerWidth - viewportPadding - halfTooltipWidth;\n }\n\n tooltipXRef.current = x;\n tooltipYRef.current = y;\n\n if (tooltipRef.current) {\n tooltipRef.current.style.setProperty(SEEK_TOOLTIP_X, `${x}px`);\n tooltipRef.current.style.setProperty(SEEK_TOOLTIP_Y, `${y}px`);\n }\n\n if (!seekState.hasInitialPosition) {\n setSeekState((prev) => ({ ...prev, hasInitialPosition: true }));\n }\n },\n [onCollisionDataUpdate, seekState.hasInitialPosition],\n );\n\n const onHoverProgressUpdate = React.useCallback(() => {\n if (!seekRef.current || seekableEnd <= 0) return;\n\n const hoverPercent = Math.min(\n 100,\n (hoverTimeRef.current / seekableEnd) * 100,\n );\n seekRef.current.style.setProperty(\n SEEK_HOVER_PERCENT,\n `${hoverPercent.toFixed(4)}%`,\n );\n }, [seekableEnd]);\n\n React.useEffect(() => {\n if (seekState.pendingSeekTime !== null) {\n const diff = Math.abs(mediaCurrentTime - seekState.pendingSeekTime);\n if (diff < 0.5) {\n setSeekState((prev) => ({ ...prev, pendingSeekTime: null }));\n }\n }\n }, [mediaCurrentTime, seekState.pendingSeekTime]);\n\n React.useEffect(() => {\n if (!seekState.isHovering || tooltipDisabled) return;\n\n function onScroll() {\n setSeekState((prev) => ({\n ...prev,\n isHovering: false,\n hasInitialPosition: false,\n }));\n dispatch({\n type: MediaActionTypes.MEDIA_PREVIEW_REQUEST,\n detail: undefined,\n });\n }\n\n document.addEventListener(\"scroll\", onScroll, { passive: true });\n return () => {\n document.removeEventListener(\"scroll\", onScroll);\n };\n }, [dispatch, seekState.isHovering, tooltipDisabled]);\n\n const bufferedProgress = React.useMemo(() => {\n if (mediaBuffered.length === 0 || seekableEnd <= 0) return 0;\n\n if (mediaEnded) return 1;\n\n const containingRange = mediaBuffered.find(\n ([start, end]) => start <= mediaCurrentTime && mediaCurrentTime <= end,\n );\n\n if (containingRange) {\n return Math.min(1, containingRange[1] / seekableEnd);\n }\n\n return Math.min(1, seekableStart / seekableEnd);\n }, [mediaBuffered, mediaCurrentTime, seekableEnd, mediaEnded, seekableStart]);\n\n const onPointerEnter = React.useCallback(() => {\n if (seekRef.current) {\n seekRectRef.current = seekRef.current.getBoundingClientRect();\n }\n\n collisionDataRef.current = null;\n pointerEnterTimeRef.current = Date.now();\n horizontalMovementRef.current = 0;\n verticalMovementRef.current = 0;\n\n if (seekableEnd > 0) {\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current);\n }\n\n if (!tooltipDisabled) {\n if (lastPointerXRef.current && seekRectRef.current) {\n const clientX = Math.max(\n seekRectRef.current.left,\n Math.min(lastPointerXRef.current, seekRectRef.current.right),\n );\n onTooltipPositionUpdate(clientX);\n }\n }\n }\n }, [seekableEnd, onTooltipPositionUpdate, tooltipDisabled]);\n\n const onPointerLeave = React.useCallback(() => {\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current);\n hoverTimeoutRef.current = null;\n }\n if (rafIdRef.current) {\n cancelAnimationFrame(rafIdRef.current);\n rafIdRef.current = null;\n }\n if (previewDebounceRef.current) {\n cancelAnimationFrame(previewDebounceRef.current);\n previewDebounceRef.current = null;\n }\n\n setSeekState((prev) => ({\n ...prev,\n isHovering: false,\n hasInitialPosition: false,\n }));\n\n justCommittedRef.current = false;\n seekRectRef.current = null;\n collisionDataRef.current = null;\n\n pointerEnterTimeRef.current = 0;\n horizontalMovementRef.current = 0;\n verticalMovementRef.current = 0;\n lastPointerXRef.current = 0;\n lastPointerYRef.current = 0;\n lastSeekCommitTimeRef.current = 0;\n\n if (!tooltipDisabled) {\n dispatch({\n type: MediaActionTypes.MEDIA_PREVIEW_REQUEST,\n detail: undefined,\n });\n }\n }, [dispatch, tooltipDisabled]);\n\n const onPointerMove = React.useCallback(\n (event: React.PointerEvent) => {\n if (seekableEnd <= 0) return;\n\n if (!seekRectRef.current && seekRef.current) {\n seekRectRef.current = seekRef.current.getBoundingClientRect();\n }\n\n if (!seekRectRef.current) return;\n\n const currentX = event.clientX;\n const currentY = event.clientY;\n\n if (lastPointerXRef.current !== 0 && lastPointerYRef.current !== 0) {\n const deltaX = Math.abs(currentX - lastPointerXRef.current);\n const deltaY = Math.abs(currentY - lastPointerYRef.current);\n\n horizontalMovementRef.current += deltaX;\n verticalMovementRef.current += deltaY;\n }\n\n lastPointerXRef.current = currentX;\n lastPointerYRef.current = currentY;\n\n if (rafIdRef.current) {\n cancelAnimationFrame(rafIdRef.current);\n }\n\n rafIdRef.current = requestAnimationFrame(() => {\n const wasJustCommitted = justCommittedRef.current;\n if (wasJustCommitted) {\n justCommittedRef.current = false;\n }\n\n const seekRect = seekRectRef.current;\n if (!seekRect) {\n rafIdRef.current = null;\n return;\n }\n\n const clientX = lastPointerXRef.current;\n const offsetXOnSeekBar = Math.max(\n 0,\n Math.min(clientX - seekRect.left, seekRect.width),\n );\n const relativeX = offsetXOnSeekBar / seekRect.width;\n const calculatedHoverTime = relativeX * seekableEnd;\n\n hoverTimeRef.current = calculatedHoverTime;\n\n onHoverProgressUpdate();\n\n const wasHovering = seekState.isHovering;\n const isCurrentlyHovering =\n clientX >= seekRect.left && clientX <= seekRect.right;\n\n const timeHovering = Date.now() - pointerEnterTimeRef.current;\n const totalMovement =\n horizontalMovementRef.current + verticalMovementRef.current;\n const horizontalRatio =\n totalMovement > 0 ? horizontalMovementRef.current / totalMovement : 0;\n\n const timeSinceSeekCommit = Date.now() - lastSeekCommitTimeRef.current;\n const isInSeekCooldown = timeSinceSeekCommit < 300;\n\n const shouldShowTooltip =\n !wasJustCommitted &&\n !isInSeekCooldown &&\n (timeHovering > 150 ||\n horizontalRatio > 0.6 ||\n (totalMovement < 10 && timeHovering > 50));\n\n if (\n !wasHovering &&\n isCurrentlyHovering &&\n shouldShowTooltip &&\n !tooltipDisabled\n ) {\n setSeekState((prev) => ({ ...prev, isHovering: true }));\n }\n\n if (!tooltipDisabled) {\n onPreviewUpdate(calculatedHoverTime);\n\n if (isCurrentlyHovering && (wasHovering || shouldShowTooltip)) {\n onTooltipPositionUpdate(clientX);\n }\n }\n\n rafIdRef.current = null;\n });\n },\n [\n onPreviewUpdate,\n onTooltipPositionUpdate,\n onHoverProgressUpdate,\n seekableEnd,\n seekState.isHovering,\n tooltipDisabled,\n ],\n );\n\n const onSeek = React.useCallback(\n (value: number | readonly number[]) => {\n const time = (Array.isArray(value) ? value[0] : value) ?? 0;\n\n setSeekState((prev) => ({ ...prev, pendingSeekTime: time }));\n\n if (!store.getState().dragging) {\n store.setState(\"dragging\", true);\n }\n\n if (seekThrottleRef.current) {\n cancelAnimationFrame(seekThrottleRef.current);\n }\n\n seekThrottleRef.current = requestAnimationFrame(() => {\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: time,\n });\n seekThrottleRef.current = null;\n });\n },\n [dispatch, store.getState, store.setState],\n );\n\n const onSeekCommit = React.useCallback(\n (value: number | readonly number[]) => {\n const time = (Array.isArray(value) ? value[0] : value) ?? 0;\n\n if (seekThrottleRef.current) {\n cancelAnimationFrame(seekThrottleRef.current);\n seekThrottleRef.current = null;\n }\n\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current);\n hoverTimeoutRef.current = null;\n }\n if (rafIdRef.current) {\n cancelAnimationFrame(rafIdRef.current);\n rafIdRef.current = null;\n }\n if (previewDebounceRef.current) {\n cancelAnimationFrame(previewDebounceRef.current);\n previewDebounceRef.current = null;\n }\n\n setSeekState((prev) => ({\n ...prev,\n pendingSeekTime: time,\n isHovering: false,\n hasInitialPosition: false,\n }));\n\n justCommittedRef.current = true;\n collisionDataRef.current = null;\n lastSeekCommitTimeRef.current = Date.now();\n\n // Reset movement tracking after seek commit\n pointerEnterTimeRef.current = Date.now();\n horizontalMovementRef.current = 0;\n verticalMovementRef.current = 0;\n\n if (store.getState().dragging) {\n store.setState(\"dragging\", false);\n }\n\n dispatch({\n type: MediaActionTypes.MEDIA_SEEK_REQUEST,\n detail: time,\n });\n\n dispatch({\n type: MediaActionTypes.MEDIA_PREVIEW_REQUEST,\n detail: undefined,\n });\n },\n [dispatch, store.getState, store.setState],\n );\n\n React.useEffect(() => {\n return () => {\n if (seekThrottleRef.current) {\n cancelAnimationFrame(seekThrottleRef.current);\n }\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current);\n }\n if (rafIdRef.current) {\n cancelAnimationFrame(rafIdRef.current);\n }\n if (previewDebounceRef.current) {\n cancelAnimationFrame(previewDebounceRef.current);\n }\n };\n }, []);\n\n const currentChapterCue = getCurrentChapterCue(hoverTimeRef.current);\n const thumbnail = getThumbnail(hoverTimeRef.current);\n const hoverTime = getCachedTime(hoverTimeRef.current, seekableEnd);\n\n const chapterSeparators = React.useMemo(() => {\n if (withoutChapter || chapterCues.length <= 1 || seekableEnd <= 0) {\n return null;\n }\n\n return chapterCues.slice(1).map((chapterCue, index) => {\n const position = (chapterCue.startTime / seekableEnd) * 100;\n\n return (\n \n );\n });\n }, [chapterCues, seekableEnd, withoutChapter]);\n\n const spriteStyle = React.useMemo(() => {\n if (!thumbnail?.coords || !thumbnail?.src) {\n return {};\n }\n\n const coordX = thumbnail.coords[0];\n const coordY = thumbnail.coords[1];\n\n const spriteWidth = Number.parseFloat(thumbnail.coords[2] ?? \"0\");\n const spriteHeight = Number.parseFloat(thumbnail.coords[3] ?? \"0\");\n\n const scaleX = spriteWidth > 0 ? SPRITE_CONTAINER_WIDTH / spriteWidth : 1;\n const scaleY =\n spriteHeight > 0 ? SPRITE_CONTAINER_HEIGHT / spriteHeight : 1;\n const scale = Math.min(scaleX, scaleY);\n\n return {\n width: `${spriteWidth}px`,\n height: `${spriteHeight}px`,\n backgroundImage: `url(${thumbnail.src})`,\n backgroundPosition: `-${coordX}px -${coordY}px`,\n backgroundRepeat: \"no-repeat\",\n transform: `scale(${scale})`,\n transformOrigin: \"top left\",\n };\n }, [thumbnail?.coords, thumbnail?.src]);\n\n const SeekSlider = (\n
\n \n \n \n \n \n {seekState.isHovering && seekableEnd > 0 && (\n \n )}\n {chapterSeparators}\n \n \n \n \n {!withoutTooltip &&\n !context.withoutTooltip &&\n seekState.isHovering &&\n seekableEnd > 0 && (\n \n \n \n {thumbnail?.src && (\n \n {thumbnail.coords ? (\n
\n ) : (\n // biome-ignore lint/performance/noImgElement: dynamic thumbnail URLs from media don't work well with Next.js Image optimization\n \n )}\n
\n )}\n {currentChapterCue && (\n \n {currentChapterCue.text}\n
\n )}\n \n {tooltipTimeVariant === \"progress\"\n ? `${hoverTime} / ${duration}`\n : hoverTime}\n \n \n \n \n )}\n \n );\n\n if (withTime) {\n return (\n
\n {currentTime}\n {SeekSlider}\n {remainingTime}\n
\n );\n }\n\n return SeekSlider;\n}\n\ninterface MediaPlayerVolumeProps extends SliderPrimitive.Root.Props {\n expandable?: boolean;\n}\n\nfunction MediaPlayerVolume(props: MediaPlayerVolumeProps) {\n const { expandable = false, className, disabled, ...volumeProps } = props;\n\n const context = useMediaPlayerContext(VOLUME_NAME);\n const store = useStoreContext(VOLUME_NAME);\n const dispatch = useMediaDispatch();\n const mediaVolume = useMediaSelector((state) => state.mediaVolume ?? 1);\n const mediaMuted = useMediaSelector((state) => state.mediaMuted ?? false);\n const mediaVolumeLevel = useMediaSelector(\n (state) => state.mediaVolumeLevel ?? \"high\",\n );\n\n const sliderId = React.useId();\n const volumeTriggerId = React.useId();\n\n const isDisabled = disabled || context.disabled;\n\n const onMute = React.useCallback(() => {\n dispatch({\n type: mediaMuted\n ? MediaActionTypes.MEDIA_UNMUTE_REQUEST\n : MediaActionTypes.MEDIA_MUTE_REQUEST,\n });\n }, [dispatch, mediaMuted]);\n\n const onVolumeChange = React.useCallback(\n (value: number | readonly number[]) => {\n const volume = (Array.isArray(value) ? value[0] : value) ?? 0;\n\n if (!store.getState().dragging) {\n store.setState(\"dragging\", true);\n }\n\n dispatch({\n type: MediaActionTypes.MEDIA_VOLUME_REQUEST,\n detail: volume,\n });\n },\n [dispatch, store.getState, store.setState],\n );\n\n const onVolumeCommit = React.useCallback(\n (value: number | readonly number[]) => {\n const volume = (Array.isArray(value) ? value[0] : value) ?? 0;\n\n if (store.getState().dragging) {\n store.setState(\"dragging\", false);\n }\n\n dispatch({\n type: MediaActionTypes.MEDIA_VOLUME_REQUEST,\n detail: volume,\n });\n },\n [dispatch, store],\n );\n\n const effectiveVolume = mediaMuted ? 0 : mediaVolume;\n\n return (\n \n \n \n {mediaVolumeLevel === \"off\" || mediaMuted ? (\n \n ) : mediaVolumeLevel === \"high\" ? (\n \n ) : (\n \n )}\n \n \n \n \n \n \n \n \n \n \n \n );\n}\n\ninterface MediaPlayerTimeProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {\n variant?: \"progress\" | \"remaining\" | \"duration\";\n}\n\nfunction MediaPlayerTime(props: MediaPlayerTimeProps) {\n const { variant = \"progress\", render, className, ...timeProps } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerTime\");\n const mediaCurrentTime = useMediaSelector(\n (state) => state.mediaCurrentTime ?? 0,\n );\n const [, seekableEnd = 0] = useMediaSelector(\n (state) => state.mediaSeekable ?? [0, 0],\n );\n\n const times = React.useMemo(() => {\n if (variant === \"remaining\") {\n return {\n remaining: timeUtils.formatTime(\n seekableEnd - mediaCurrentTime,\n seekableEnd,\n ),\n };\n }\n\n if (variant === \"duration\") {\n return {\n duration: timeUtils.formatTime(seekableEnd, seekableEnd),\n };\n }\n\n return {\n current: timeUtils.formatTime(mediaCurrentTime, seekableEnd),\n duration: timeUtils.formatTime(seekableEnd, seekableEnd),\n };\n }, [variant, mediaCurrentTime, seekableEnd]);\n\n const isSingleValue = variant === \"remaining\" || variant === \"duration\";\n\n const timeChildren = isSingleValue ? (\n times[variant]\n ) : (\n <>\n {times.current}\n \n /\n \n {times.duration}\n \n );\n\n const timeClassName = isSingleValue\n ? cn(\"text-foreground/80 text-sm tabular-nums\", className)\n : cn(\"flex items-center gap-1 text-foreground/80 text-sm\", className);\n\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n dir: context.dir,\n className: timeClassName,\n children: timeChildren,\n },\n timeProps,\n ),\n render,\n state: { slot: \"media-player-time\", variant },\n });\n}\n\ninterface MediaPlayerPlaybackSpeedProps\n extends Omit, \"dir\" | \"children\">,\n Pick, \"sideOffset\">,\n React.ComponentProps {\n speeds?: number[];\n}\n\nfunction MediaPlayerPlaybackSpeed(props: MediaPlayerPlaybackSpeedProps) {\n const {\n open,\n defaultOpen,\n onOpenChange: onOpenChangeProp,\n sideOffset = FLOATING_MENU_SIDE_OFFSET,\n speeds = SPEEDS,\n modal = false,\n className,\n disabled,\n ...playbackSpeedProps\n } = props;\n\n const context = useMediaPlayerContext(PLAYBACK_SPEED_NAME);\n const store = useStoreContext(PLAYBACK_SPEED_NAME);\n const dispatch = useMediaDispatch();\n const mediaPlaybackRate = useMediaSelector(\n (state) => state.mediaPlaybackRate ?? 1,\n );\n\n const isDisabled = disabled || context.disabled;\n\n const onPlaybackRateChange = React.useCallback(\n (rate: number) => {\n dispatch({\n type: MediaActionTypes.MEDIA_PLAYBACK_RATE_REQUEST,\n detail: rate,\n });\n },\n [dispatch],\n );\n\n const onOpenChange = React.useCallback<\n NonNullable[\"onOpenChange\"]>\n >(\n (open, eventDetails) => {\n store.setState(\"menuOpen\", open);\n onOpenChangeProp?.(open, eventDetails);\n },\n [store.setState, onOpenChangeProp],\n );\n\n return (\n \n \"]}>\n \n {mediaPlaybackRate}x\n \n }\n />\n \n \n {speeds.map((speed) => (\n onPlaybackRateChange(speed)}\n >\n {speed}x{mediaPlaybackRate === speed && }\n \n ))}\n \n \n );\n}\n\ninterface MediaPlayerLoopProps extends React.ComponentProps {}\n\nfunction MediaPlayerLoop(props: MediaPlayerLoopProps) {\n const { children, className, disabled, ...loopProps } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerLoop\");\n const isDisabled = disabled || context.disabled;\n\n const [isLooping, setIsLooping] = React.useState(() => {\n const mediaElement = context.mediaRef.current;\n return mediaElement?.loop ?? false;\n });\n\n React.useEffect(() => {\n const mediaElement = context.mediaRef.current;\n if (!mediaElement) return;\n\n setIsLooping(mediaElement.loop);\n\n const checkLoop = () => setIsLooping(mediaElement.loop);\n const observer = new MutationObserver(checkLoop);\n observer.observe(mediaElement, {\n attributes: true,\n attributeFilter: [\"loop\"],\n });\n\n return () => observer.disconnect();\n }, [context.mediaRef]);\n\n const onLoopToggle = React.useCallback(\n (event) => {\n props.onClick?.(event);\n if (event.defaultPrevented) return;\n\n const mediaElement = context.mediaRef.current;\n if (mediaElement) {\n const newLoopState = !mediaElement.loop;\n mediaElement.loop = newLoopState;\n setIsLooping(newLoopState);\n }\n },\n [context.mediaRef, props.onClick],\n );\n\n return (\n \n \n {children ??\n (isLooping ? (\n \n ) : (\n \n ))}\n \n \n );\n}\n\ninterface MediaPlayerFullscreenProps\n extends React.ComponentProps {}\n\nfunction MediaPlayerFullscreen(props: MediaPlayerFullscreenProps) {\n const { children, className, disabled, ...fullscreenProps } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerFullscreen\");\n const dispatch = useMediaDispatch();\n const isFullscreen = useMediaSelector(\n (state) => state.mediaIsFullscreen ?? false,\n );\n\n const isDisabled = disabled || context.disabled;\n\n const onFullscreen = React.useCallback(\n (event) => {\n props.onClick?.(event);\n\n if (event.defaultPrevented) return;\n\n dispatch({\n type: isFullscreen\n ? MediaActionTypes.MEDIA_EXIT_FULLSCREEN_REQUEST\n : MediaActionTypes.MEDIA_ENTER_FULLSCREEN_REQUEST,\n });\n },\n [dispatch, props.onClick, isFullscreen],\n );\n\n return (\n \n \n {children ?? (isFullscreen ? : )}\n \n \n );\n}\n\ninterface MediaPlayerPiPProps\n extends Omit, \"children\"> {\n children?:\n | React.ReactNode\n | ((isPictureInPicture: boolean) => React.ReactNode);\n onPipError?: (error: unknown, state: \"enter\" | \"exit\") => void;\n}\n\nfunction MediaPlayerPiP(props: MediaPlayerPiPProps) {\n const { children, className, onPipError, disabled, ...pipButtonProps } =\n props;\n\n const context = useMediaPlayerContext(\"MediaPlayerPiP\");\n const dispatch = useMediaDispatch();\n const isPictureInPicture = useMediaSelector(\n (state) => state.mediaIsPip ?? false,\n );\n\n const isDisabled = disabled || context.disabled;\n\n const onPictureInPicture = React.useCallback(\n (event) => {\n props.onClick?.(event);\n\n if (event.defaultPrevented) return;\n\n dispatch({\n type: isPictureInPicture\n ? MediaActionTypes.MEDIA_EXIT_PIP_REQUEST\n : MediaActionTypes.MEDIA_ENTER_PIP_REQUEST,\n });\n\n const mediaElement = context.mediaRef.current;\n\n if (mediaElement instanceof HTMLVideoElement) {\n if (isPictureInPicture) {\n document.exitPictureInPicture().catch((error) => {\n onPipError?.(error, \"exit\");\n });\n } else {\n mediaElement.requestPictureInPicture().catch((error) => {\n onPipError?.(error, \"enter\");\n });\n }\n }\n },\n [dispatch, props.onClick, isPictureInPicture, onPipError, context.mediaRef],\n );\n\n return (\n \n \n {typeof children === \"function\"\n ? children(isPictureInPicture)\n : (children ??\n (isPictureInPicture ? (\n \n ) : (\n \n )))}\n \n \n );\n}\n\nfunction MediaPlayerCaptions(props: React.ComponentProps) {\n const { children, className, disabled, ...captionsProps } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerCaptions\");\n const dispatch = useMediaDispatch();\n const isSubtitlesActive = useMediaSelector(\n (state) => (state.mediaSubtitlesShowing ?? []).length > 0,\n );\n\n const isDisabled = disabled || context.disabled;\n const onCaptionsToggle = React.useCallback(\n (event) => {\n props.onClick?.(event);\n\n if (event.defaultPrevented) return;\n\n dispatch({\n type: MediaActionTypes.MEDIA_TOGGLE_SUBTITLES_REQUEST,\n });\n },\n [dispatch, props.onClick],\n );\n\n return (\n \n \n {children ??\n (isSubtitlesActive ? : )}\n \n \n );\n}\n\nfunction MediaPlayerDownload(props: React.ComponentProps) {\n const { children, className, disabled, ...downloadProps } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerDownload\");\n\n const isDisabled = disabled || context.disabled;\n\n const onDownload = React.useCallback(\n (event) => {\n props.onClick?.(event);\n\n if (event.defaultPrevented) return;\n\n const mediaElement = context.mediaRef.current;\n\n if (!mediaElement?.currentSrc) return;\n\n const link = document.createElement(\"a\");\n link.href = mediaElement.currentSrc;\n link.download = \"\";\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n },\n [context.mediaRef, props.onClick],\n );\n\n return (\n \n \n {children ?? }\n \n \n );\n}\n\ninterface MediaPlayerSettingsProps extends MediaPlayerPlaybackSpeedProps {}\n\nfunction MediaPlayerSettings(props: MediaPlayerSettingsProps) {\n const {\n open,\n defaultOpen,\n onOpenChange: onOpenChangeProp,\n sideOffset = FLOATING_MENU_SIDE_OFFSET,\n speeds = SPEEDS,\n modal = false,\n className,\n disabled,\n ...settingsProps\n } = props;\n\n const context = useMediaPlayerContext(SETTINGS_NAME);\n const store = useStoreContext(SETTINGS_NAME);\n const dispatch = useMediaDispatch();\n\n const mediaPlaybackRate = useMediaSelector(\n (state) => state.mediaPlaybackRate ?? 1,\n );\n const mediaSubtitlesList = useMediaSelector(\n (state) => state.mediaSubtitlesList ?? [],\n );\n const mediaSubtitlesShowing = useMediaSelector(\n (state) => state.mediaSubtitlesShowing ?? [],\n );\n const mediaRenditionList = useMediaSelector(\n (state) => state.mediaRenditionList ?? [],\n );\n const selectedRenditionId = useMediaSelector(\n (state) => state.mediaRenditionSelected,\n );\n\n const isDisabled = disabled || context.disabled;\n const isSubtitlesActive = mediaSubtitlesShowing.length > 0;\n\n const onPlaybackRateChange = React.useCallback(\n (rate: number) => {\n dispatch({\n type: MediaActionTypes.MEDIA_PLAYBACK_RATE_REQUEST,\n detail: rate,\n });\n },\n [dispatch],\n );\n\n const onRenditionChange = React.useCallback(\n (renditionId: string) => {\n dispatch({\n type: MediaActionTypes.MEDIA_RENDITION_REQUEST,\n detail: renditionId === \"auto\" ? undefined : renditionId,\n });\n },\n [dispatch],\n );\n\n const onSubtitlesToggle = React.useCallback(() => {\n dispatch({\n type: MediaActionTypes.MEDIA_TOGGLE_SUBTITLES_REQUEST,\n detail: false,\n });\n }, [dispatch]);\n\n const onShowSubtitleTrack = React.useCallback(\n (subtitleTrack: (typeof mediaSubtitlesList)[number]) => {\n dispatch({\n type: MediaActionTypes.MEDIA_TOGGLE_SUBTITLES_REQUEST,\n detail: false,\n });\n dispatch({\n type: MediaActionTypes.MEDIA_SHOW_SUBTITLES_REQUEST,\n detail: subtitleTrack,\n });\n },\n [dispatch],\n );\n\n const selectedSubtitleLabel = React.useMemo(() => {\n if (!isSubtitlesActive) return \"Off\";\n if (mediaSubtitlesShowing.length > 0) {\n return mediaSubtitlesShowing[0]?.label ?? \"On\";\n }\n return \"Off\";\n }, [isSubtitlesActive, mediaSubtitlesShowing]);\n\n const selectedRenditionLabel = React.useMemo(() => {\n if (!selectedRenditionId) return \"Auto\";\n\n const currentRendition = mediaRenditionList?.find(\n (rendition) => rendition.id === selectedRenditionId,\n );\n if (!currentRendition) return \"Auto\";\n\n if (currentRendition.height) return `${currentRendition.height}p`;\n if (currentRendition.width) return `${currentRendition.width}p`;\n return currentRendition.id ?? \"Auto\";\n }, [selectedRenditionId, mediaRenditionList]);\n\n const onOpenChange = React.useCallback<\n NonNullable[\"onOpenChange\"]>\n >(\n (open, eventDetails) => {\n store.setState(\"menuOpen\", open);\n onOpenChangeProp?.(open, eventDetails);\n },\n [store.setState, onOpenChangeProp],\n );\n\n return (\n \n \n \n \n \n }\n />\n \n \n Settings\n \n \n Speed\n \n {mediaPlaybackRate}x\n \n \n \n {speeds.map((speed) => (\n onPlaybackRateChange(speed)}\n >\n {speed}x{mediaPlaybackRate === speed && }\n \n ))}\n \n \n {context.isVideo && mediaRenditionList.length > 0 && (\n \n \n Quality\n \n {selectedRenditionLabel}\n \n \n \n onRenditionChange(\"auto\")}\n >\n Auto\n {!selectedRenditionId && }\n \n {mediaRenditionList\n .slice()\n .sort((a, b) => {\n const aHeight = a.height ?? 0;\n const bHeight = b.height ?? 0;\n return bHeight - aHeight;\n })\n .map((rendition) => {\n const label = rendition.height\n ? `${rendition.height}p`\n : rendition.width\n ? `${rendition.width}p`\n : (rendition.id ?? \"Unknown\");\n\n const selected = rendition.id === selectedRenditionId;\n\n return (\n onRenditionChange(rendition.id ?? \"\")}\n >\n {label}\n {selected && }\n \n );\n })}\n \n \n )}\n \n \n Captions\n \n {selectedSubtitleLabel}\n \n \n \n \n Off\n {!isSubtitlesActive && }\n \n {mediaSubtitlesList.map((subtitleTrack) => {\n const isSelected = mediaSubtitlesShowing.some(\n (showingSubtitle) =>\n showingSubtitle.label === subtitleTrack.label,\n );\n return (\n onShowSubtitleTrack(subtitleTrack)}\n >\n {subtitleTrack.label}\n {isSelected && }\n \n );\n })}\n {mediaSubtitlesList.length === 0 && (\n \n No captions available\n \n )}\n \n \n \n \n );\n}\n\ninterface MediaPlayerPortalProps {\n container?: Element | DocumentFragment | null;\n children?: React.ReactNode;\n}\n\nfunction MediaPlayerPortal(props: MediaPlayerPortalProps) {\n const { container: containerProp, children } = props;\n\n const context = useMediaPlayerContext(\"MediaPlayerPortal\");\n const container = containerProp ?? context.portalContainer;\n\n if (!container) return null;\n\n return ReactDOM.createPortal(children, container);\n}\n\ninterface MediaPlayerTooltipProps\n extends Omit, \"children\">,\n Pick, \"sideOffset\">,\n Pick, \"delay\"> {\n tooltip?: string;\n shortcut?: string | string[];\n children?: React.ReactNode;\n}\n\nfunction MediaPlayerTooltip(props: MediaPlayerTooltipProps) {\n const { tooltip, shortcut, delay, sideOffset, children, ...tooltipProps } =\n props;\n\n const context = useMediaPlayerContext(\"MediaPlayerTooltip\");\n const tooltipDelay = delay ?? context.tooltipDelay;\n const tooltipSideOffset = sideOffset ?? context.tooltipSideOffset;\n\n if ((!tooltip && !shortcut) || context.withoutTooltip) return <>{children};\n\n const trigger = React.isValidElement(children) ? (\n \n ) : (\n \n {children}\n \n );\n\n return (\n \n \n {trigger}\n span]:hidden\"\n >\n

{tooltip}

\n {Array.isArray(shortcut) ? (\n
\n {shortcut.map((shortcutKey) => (\n \n \n {shortcutKey}\n \n \n ))}\n
\n ) : (\n shortcut && (\n \n \n {shortcut}\n \n \n )\n )}\n \n
\n
\n );\n}\n\nexport {\n MediaPlayer,\n MediaPlayerAudio,\n MediaPlayerCaptions,\n MediaPlayerControls,\n MediaPlayerControlsOverlay,\n MediaPlayerDownload,\n MediaPlayerError,\n MediaPlayerFullscreen,\n MediaPlayerLoading,\n MediaPlayerLoop,\n MediaPlayerPiP,\n MediaPlayerPlay,\n MediaPlayerPlaybackSpeed,\n MediaPlayerPortal,\n type MediaPlayerProps,\n MediaPlayerSeek,\n MediaPlayerSeekBackward,\n MediaPlayerSeekForward,\n MediaPlayerSettings,\n MediaPlayerTime,\n MediaPlayerTooltip,\n MediaPlayerVideo,\n MediaPlayerVolume,\n MediaPlayerVolumeIndicator,\n useMediaSelector as useMediaPlayer,\n useStore as useMediaPlayerStore,\n};\n", "target": "" } ], "registryDependencies": [ "badge", "button", "dropdown-menu", "tooltip", "direction", "@diceui/compose-refs", "@diceui/use-lazy-ref" ], "dependencies": [ "@base-ui/react", "media-chrome" ] }