{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-media-session",
  "files": [
    {
      "path": "registry/default/hooks/use-media-session.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nexport type MediaSessionActionHandlers = Partial<\n  Record<MediaSessionActionName, MediaSessionActionHandler | null>\n>\n\nexport type MediaSessionActionName =\n  | \"enterpictureinpicture\"\n  | \"hangup\"\n  | \"nextslide\"\n  | \"previousslide\"\n  | \"togglecamera\"\n  | \"togglemicrophone\"\n  | \"togglescreenshare\"\n  | MediaSessionAction\n\nexport interface MediaSessionChapterInformationInit {\n  artwork?: MediaImage[]\n  startTime?: number\n  title?: string\n}\n\nexport type MediaSessionMetadata = MediaMetadataInit & {\n  chapterInfo?: MediaSessionChapterInformationInit[]\n}\n\nexport interface MediaSessionPictureInPictureActionDetails extends MediaSessionActionDetails {\n  enterPictureInPictureReason?: \"contentoccluded\" | \"other\" | \"useraction\"\n}\n\nexport interface MediaSessionPlaylistNavigation {\n  getNextIndex: () => number\n  getPreviousIndex: () => number\n  queue: readonly unknown[]\n  repeatMode: \"all\" | \"off\" | \"one\"\n}\n\nexport type MediaSessionSourceType = \"asset\" | \"playlist\" | null\n\nexport interface UseMediaSessionActionHandlersOptions {\n  canEnterPictureInPicture?: boolean\n  canGoNext?: boolean\n  canGoPrevious?: boolean\n  getCurrentTime: () => number\n  getSeekRange?: () => null | undefined | { start: number }\n  onEnterPictureInPicture?: (\n    details: MediaSessionPictureInPictureActionDetails\n  ) => MaybePromise<void>\n  onNextTrack?: () => MaybePromise<void>\n  onPause: () => MaybePromise<void>\n  onPlay: () => MaybePromise<void>\n  onPreviousTrack?: () => MaybePromise<void>\n  onSeek: (\n    time: number,\n    details: MediaSessionActionDetails\n  ) => MaybePromise<void>\n  onSkipAd?: () => MaybePromise<void>\n  onStop?: () => MaybePromise<void>\n  seekOffset?: number\n}\n\nexport interface UseMediaSessionReturn {\n  clearMetadata: () => void\n  clearPositionState: () => void\n  setActionHandler: (\n    action: MediaSessionActionName,\n    handler: MediaSessionActionHandler | null\n  ) => () => void\n  setMetadata: (metadata: MediaSessionMetadata) => void\n  setPlaybackState: (state: MediaSessionPlaybackState) => void\n  setPositionState: (state: MediaPositionState) => void\n  supported: boolean\n}\n\nexport interface UseMediaSessionSyncOptions {\n  actions?: MediaSessionActionHandlers\n  active?: boolean\n  claim?: boolean\n  metadata?: MediaSessionMetadata | null\n  playbackState?: MediaSessionPlaybackState\n  position?: MediaPositionState | null\n  positionKey?: null | number | string\n}\n\ntype MaybePromise<T> = Promise<T> | T\ntype RetainedMediaPositionState = {\n  key: null | number | string\n  position: MediaPositionState\n}\n\nconst DEFAULT_MEDIA_SESSION_SEEK_OFFSET_SECONDS = 10\nconst FROZEN_MEDIA_SESSION_PLAYBACK_RATE = 0.000001\nconst MEDIA_SESSION_OWNER_FALLBACK = Symbol(\"limeplay-media-session-owner\")\n\nlet mediaSessionOwner: null | symbol = null\nconst mediaSessionOwnerListeners = new Set<() => void>()\n\nexport function canMoveToNextMediaSessionTrack(\n  sourceType: MediaSessionSourceType,\n  playlist: MediaSessionPlaylistNavigation\n): boolean {\n  return (\n    isMediaSessionPlaylistSource(sourceType) &&\n    hasNextMediaSessionPlaylistItem(playlist)\n  )\n}\n\nexport function canMoveToPreviousMediaSessionTrack(\n  sourceType: MediaSessionSourceType,\n  playlist: MediaSessionPlaylistNavigation\n): boolean {\n  return (\n    isMediaSessionPlaylistSource(sourceType) &&\n    hasPreviousMediaSessionPlaylistItem(playlist)\n  )\n}\n\nexport function canStartMediaSessionPlayback(status: string): boolean {\n  return ![\"error\", \"init\"].includes(status)\n}\n\nexport function getMediaSessionCurrentTime({\n  currentTime,\n  isLive,\n  mediaElement,\n}: {\n  currentTime: number\n  isLive: boolean\n  mediaElement: HTMLMediaElement | null\n}): number {\n  return isLive && mediaElement ? mediaElement.currentTime : currentTime\n}\n\nexport function getMediaSessionMetadataValue(\n  ...values: (null | number | string | undefined)[]\n): string | undefined {\n  for (const value of values) {\n    if (typeof value === \"number\" && Number.isFinite(value)) {\n      return String(value)\n    }\n\n    if (typeof value !== \"string\") continue\n\n    const trimmed = value.trim()\n    if (trimmed) return trimmed\n  }\n\n  return undefined\n}\n\nexport function getMediaSessionPlaybackState({\n  active,\n  status,\n}: {\n  active: boolean\n  status: string\n}): MediaSessionPlaybackState {\n  if (!active) return \"none\"\n\n  return status === \"playing\" ? \"playing\" : \"paused\"\n}\n\nexport function getMediaSessionPositionState({\n  active,\n  currentTime,\n  duration,\n  playbackRate = 1,\n}: {\n  active: boolean\n  currentTime: number\n  duration: number\n  playbackRate?: number\n}): MediaPositionState | null {\n  if (!active) return null\n  if (!isValidFiniteDuration(duration)) return null\n  if (!Number.isFinite(currentTime) || currentTime < 0) return null\n  if (!isValidPlaybackRate(playbackRate)) return null\n\n  return {\n    duration,\n    playbackRate,\n    position: getSafePosition(currentTime, duration),\n  }\n}\n\nexport function isMediaSessionPlaylistSource(\n  sourceType: MediaSessionSourceType\n): boolean {\n  return sourceType === \"playlist\"\n}\n\nexport function useMediaSession(): UseMediaSessionReturn {\n  const supported = isMediaSessionSupported()\n\n  const clearMetadata = React.useCallback(() => {\n    const session = getMediaSession()\n    if (!session) return\n\n    session.metadata = null\n  }, [])\n\n  const clearPositionState = React.useCallback(() => {\n    const session = getMediaSession()\n    if (!session) return\n\n    try {\n      session.setPositionState()\n    } catch (error) {\n      console.warn(\"[useMediaSession] Failed to clear position state:\", error)\n    }\n  }, [])\n\n  const setActionHandler = React.useCallback(\n    (\n      action: MediaSessionActionName,\n      handler: MediaSessionActionHandler | null\n    ) => {\n      const session = getMediaSession()\n      if (!session) return noop\n\n      setSafeActionHandler(session, action, handler)\n\n      return () => {\n        setSafeActionHandler(session, action, null)\n      }\n    },\n    []\n  )\n\n  const setMetadata = React.useCallback((metadata: MediaSessionMetadata) => {\n    const session = getMediaSession()\n    if (\n      !session ||\n      typeof window === \"undefined\" ||\n      typeof window.MediaMetadata !== \"function\"\n    ) {\n      return\n    }\n\n    try {\n      session.metadata = new window.MediaMetadata(metadata)\n    } catch (error) {\n      console.warn(\"[useMediaSession] Failed to set metadata:\", error)\n    }\n  }, [])\n\n  const setPlaybackState = React.useCallback(\n    (state: MediaSessionPlaybackState) => {\n      const session = getMediaSession()\n      if (!session) return\n\n      try {\n        session.playbackState = state\n      } catch (error) {\n        console.warn(\"[useMediaSession] Failed to set playback state:\", error)\n      }\n    },\n    []\n  )\n\n  const setPositionState = React.useCallback((state: MediaPositionState) => {\n    const session = getMediaSession()\n    if (!session) return\n\n    const safeState = normalizePositionState(state)\n    if (!safeState) return\n\n    try {\n      session.setPositionState(safeState)\n    } catch (error) {\n      console.warn(\"[useMediaSession] Failed to set position state:\", error)\n    }\n  }, [])\n\n  return React.useMemo(\n    () => ({\n      clearMetadata,\n      clearPositionState,\n      setActionHandler,\n      setMetadata,\n      setPlaybackState,\n      setPositionState,\n      supported,\n    }),\n    [\n      clearMetadata,\n      clearPositionState,\n      setActionHandler,\n      setMetadata,\n      setPlaybackState,\n      setPositionState,\n      supported,\n    ]\n  )\n}\n\nexport function useMediaSessionActionHandlers(\n  options: UseMediaSessionActionHandlersOptions\n): MediaSessionActionHandlers {\n  const optionsRef = React.useRef(options)\n  optionsRef.current = options\n\n  const canEnterPictureInPicture = Boolean(\n    options.canEnterPictureInPicture && options.onEnterPictureInPicture\n  )\n  const canGoNext = Boolean(options.canGoNext && options.onNextTrack)\n  const canGoPrevious = Boolean(\n    options.canGoPrevious && options.onPreviousTrack\n  )\n  const canSkipAd = Boolean(options.onSkipAd)\n  const canStop = Boolean(options.onStop)\n\n  return React.useMemo(\n    () => ({\n      enterpictureinpicture: canEnterPictureInPicture\n        ? (details) => {\n            runMediaSessionAction(\n              () =>\n                optionsRef.current.onEnterPictureInPicture?.(\n                  details as MediaSessionPictureInPictureActionDetails\n                ),\n              \"enterpictureinpicture\"\n            )\n          }\n        : null,\n      nexttrack: canGoNext\n        ? () => {\n            runMediaSessionAction(\n              () => optionsRef.current.onNextTrack?.(),\n              \"nexttrack\"\n            )\n          }\n        : null,\n      pause: () => {\n        runMediaSessionAction(() => optionsRef.current.onPause(), \"pause\")\n      },\n      play: () => {\n        runMediaSessionAction(() => optionsRef.current.onPlay(), \"play\")\n      },\n      previoustrack: canGoPrevious\n        ? () => {\n            runMediaSessionAction(\n              () => optionsRef.current.onPreviousTrack?.(),\n              \"previoustrack\"\n            )\n          }\n        : null,\n      seekbackward: (details) => {\n        const currentOptions = optionsRef.current\n        runMediaSessionAction(\n          () =>\n            seekByOffset(\n              currentOptions.getCurrentTime(),\n              -(details.seekOffset ?? getSeekOffset(currentOptions)),\n              currentOptions.onSeek,\n              details\n            ),\n          \"seekbackward\"\n        )\n      },\n      seekforward: (details) => {\n        const currentOptions = optionsRef.current\n        runMediaSessionAction(\n          () =>\n            seekByOffset(\n              currentOptions.getCurrentTime(),\n              details.seekOffset ?? getSeekOffset(currentOptions),\n              currentOptions.onSeek,\n              details\n            ),\n          \"seekforward\"\n        )\n      },\n      seekto: (details) => {\n        if (typeof details.seekTime !== \"number\") return\n\n        const currentOptions = optionsRef.current\n        const seekRange = currentOptions.getSeekRange?.()\n        const seekTime = seekRange\n          ? seekRange.start + details.seekTime\n          : details.seekTime\n\n        runMediaSessionAction(\n          () => currentOptions.onSeek(seekTime, details),\n          \"seekto\"\n        )\n      },\n      skipad: canSkipAd\n        ? () => {\n            runMediaSessionAction(\n              () => optionsRef.current.onSkipAd?.(),\n              \"skipad\"\n            )\n          }\n        : null,\n      stop: canStop\n        ? () => {\n            runMediaSessionAction(() => optionsRef.current.onStop?.(), \"stop\")\n          }\n        : null,\n    }),\n    [canEnterPictureInPicture, canGoNext, canGoPrevious, canSkipAd, canStop]\n  )\n}\n\nexport function useMediaSessionSync({\n  actions,\n  active = true,\n  claim = false,\n  metadata,\n  playbackState,\n  position,\n  positionKey = null,\n}: UseMediaSessionSyncOptions): UseMediaSessionReturn {\n  const mediaSession = useMediaSession()\n  const owner = React.useRef<symbol>(MEDIA_SESSION_OWNER_FALLBACK)\n  const retainedPosition = React.useRef<null | RetainedMediaPositionState>(null)\n  const currentOwner = React.useSyncExternalStore(\n    subscribeMediaSessionOwner,\n    getMediaSessionOwner,\n    getServerMediaSessionOwner\n  )\n  const ownsMediaSession = active && currentOwner === owner.current\n\n  if (owner.current === MEDIA_SESSION_OWNER_FALLBACK) {\n    owner.current = Symbol(\"limeplay-media-session-owner\")\n  }\n\n  React.useEffect(() => {\n    if (!active) {\n      if (getMediaSessionOwner() === owner.current) {\n        mediaSession.clearMetadata()\n        mediaSession.clearPositionState()\n        mediaSession.setPlaybackState(\"none\")\n        retainedPosition.current = null\n        releaseMediaSessionOwner(owner.current)\n      }\n      return\n    }\n\n    claimMediaSessionOwner(owner.current, claim)\n  }, [active, claim, currentOwner, mediaSession])\n\n  React.useEffect(() => {\n    if (!ownsMediaSession) return\n\n    const cleanupHandlers = getActionEntries(actions).map(([action, handler]) =>\n      mediaSession.setActionHandler(action, handler ?? null)\n    )\n\n    return () => {\n      if (getMediaSessionOwner() !== owner.current) return\n\n      cleanupHandlers.forEach((cleanup) => cleanup())\n    }\n  }, [actions, mediaSession, ownsMediaSession])\n\n  // Publish metadata after the current action set so platform UIs choose the\n  // correct track-navigation or seek-control layout for this source. Republish\n  // when actions change even if the metadata values stay the same.\n  React.useEffect(() => {\n    if (!ownsMediaSession) return\n\n    if (!metadata) {\n      mediaSession.clearMetadata()\n      retainedPosition.current = null\n      return\n    }\n\n    mediaSession.setMetadata(metadata)\n  }, [actions, mediaSession, metadata, ownsMediaSession])\n\n  React.useEffect(() => {\n    if (!ownsMediaSession) return\n\n    const nextPlaybackState = playbackState ?? \"none\"\n    const nextPosition =\n      position ??\n      (nextPlaybackState === \"playing\" ||\n      retainedPosition.current?.key !== positionKey\n        ? null\n        : retainedPosition.current.position)\n\n    if (position) {\n      retainedPosition.current = {\n        key: positionKey,\n        position,\n      }\n    }\n\n    mediaSession.setPlaybackState(nextPlaybackState)\n\n    if (nextPlaybackState === \"none\" || !nextPosition) {\n      mediaSession.clearPositionState()\n      return\n    }\n\n    mediaSession.setPositionState(\n      getPositionStateForPlaybackState(nextPosition, nextPlaybackState)\n    )\n  }, [mediaSession, ownsMediaSession, playbackState, position, positionKey])\n\n  React.useEffect(() => {\n    return () => {\n      if (getMediaSessionOwner() === owner.current) {\n        mediaSession.clearMetadata()\n        mediaSession.clearPositionState()\n        mediaSession.setPlaybackState(\"none\")\n        retainedPosition.current = null\n        releaseMediaSessionOwner(owner.current)\n      }\n    }\n  }, [mediaSession])\n\n  return mediaSession\n}\n\nfunction claimMediaSessionOwner(owner: symbol, force: boolean) {\n  if (mediaSessionOwner === owner) return\n  if (mediaSessionOwner !== null && !force) return\n\n  mediaSessionOwner = owner\n  notifyMediaSessionOwnerListeners()\n}\n\nfunction getActionEntries(\n  actions: MediaSessionActionHandlers | undefined\n): [MediaSessionActionName, MediaSessionActionHandler | null | undefined][] {\n  return Object.entries(actions ?? {}) as [\n    MediaSessionActionName,\n    MediaSessionActionHandler | null | undefined,\n  ][]\n}\n\nfunction getMediaSession(): MediaSession | null {\n  if (!isMediaSessionSupported()) return null\n\n  const mediaSession = navigator.mediaSession as MediaSession | undefined\n\n  return mediaSession ?? null\n}\n\nfunction getMediaSessionOwner(): null | symbol {\n  return mediaSessionOwner\n}\n\nfunction getPositionStateForPlaybackState(\n  position: MediaPositionState,\n  playbackState: MediaSessionPlaybackState\n): MediaPositionState {\n  if (playbackState === \"playing\") return position\n\n  return {\n    ...position,\n    playbackRate: FROZEN_MEDIA_SESSION_PLAYBACK_RATE,\n  }\n}\n\nfunction getSafePosition(position: number, duration: number): number {\n  return Number.isFinite(duration) ? Math.min(position, duration) : position\n}\n\nfunction getSeekOffset({\n  seekOffset,\n}: UseMediaSessionActionHandlersOptions): number {\n  return seekOffset ?? DEFAULT_MEDIA_SESSION_SEEK_OFFSET_SECONDS\n}\n\nfunction getServerMediaSessionOwner(): null | symbol {\n  return null\n}\n\nfunction hasNextMediaSessionPlaylistItem(\n  playlist: MediaSessionPlaylistNavigation\n): boolean {\n  if (playlist.repeatMode === \"all\" && playlist.queue.length > 0) return true\n\n  return playlist.getNextIndex() !== -1\n}\n\nfunction hasPreviousMediaSessionPlaylistItem(\n  playlist: MediaSessionPlaylistNavigation\n): boolean {\n  if (playlist.repeatMode === \"all\" && playlist.queue.length > 0) return true\n\n  return playlist.getPreviousIndex() !== -1\n}\n\nfunction isMediaSessionSupported(): boolean {\n  return typeof navigator !== \"undefined\" && \"mediaSession\" in navigator\n}\n\nfunction isValidFiniteDuration(duration: number): boolean {\n  return Number.isFinite(duration) && duration > 0\n}\n\nfunction isValidPlaybackRate(playbackRate: number): boolean {\n  return Number.isFinite(playbackRate) && playbackRate !== 0\n}\n\nfunction noop() {}\n\nfunction normalizePositionState(\n  state: MediaPositionState\n): MediaPositionState | null {\n  const nextState: MediaPositionState = {}\n  const hasPositionData =\n    typeof state.duration === \"number\" ||\n    typeof state.playbackRate === \"number\" ||\n    typeof state.position === \"number\"\n\n  if (!hasPositionData) return nextState\n\n  if (\n    typeof state.duration !== \"number\" ||\n    !isValidFiniteDuration(state.duration)\n  ) {\n    return null\n  }\n\n  nextState.duration = state.duration\n\n  if (typeof state.playbackRate === \"number\") {\n    if (!isValidPlaybackRate(state.playbackRate)) return null\n\n    nextState.playbackRate = state.playbackRate\n  }\n\n  if (typeof state.position === \"number\") {\n    if (!Number.isFinite(state.position) || state.position < 0) return null\n\n    nextState.position = getSafePosition(state.position, nextState.duration)\n  }\n\n  return nextState\n}\n\nfunction notifyMediaSessionOwnerListeners() {\n  mediaSessionOwnerListeners.forEach((listener) => listener())\n}\n\nfunction releaseMediaSessionOwner(owner: symbol) {\n  if (mediaSessionOwner !== owner) return\n\n  mediaSessionOwner = null\n  notifyMediaSessionOwnerListeners()\n}\n\nfunction runMediaSessionAction(\n  action: () => MaybePromise<void>,\n  name: MediaSessionActionName\n) {\n  try {\n    void Promise.resolve(action()).catch((error) => {\n      console.warn(`[useMediaSession] \"${name}\" action failed:`, error)\n    })\n  } catch (error) {\n    console.warn(`[useMediaSession] \"${name}\" action failed:`, error)\n  }\n}\n\nfunction seekByOffset(\n  currentTime: number,\n  offset: number,\n  seek: (\n    time: number,\n    details: MediaSessionActionDetails\n  ) => MaybePromise<void>,\n  details: MediaSessionActionDetails\n) {\n  if (!Number.isFinite(offset)) return\n  if (!Number.isFinite(currentTime)) return\n\n  return seek(currentTime + offset, details)\n}\n\nfunction setSafeActionHandler(\n  session: MediaSession,\n  action: MediaSessionActionName,\n  handler: MediaSessionActionHandler | null\n) {\n  try {\n    session.setActionHandler(action as MediaSessionAction, handler)\n  } catch (error) {\n    console.warn(\n      `[useMediaSession] Failed to set \"${action}\" action handler:`,\n      error\n    )\n  }\n}\n\nfunction subscribeMediaSessionOwner(listener: () => void): () => void {\n  mediaSessionOwnerListeners.add(listener)\n\n  return () => {\n    mediaSessionOwnerListeners.delete(listener)\n  }\n}\n",
      "type": "registry:hook",
      "target": "hooks/limeplay/use-media-session.ts"
    }
  ],
  "type": "registry:hook"
}