{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "video-player",
  "title": "Video Player",
  "description": "Dependency-free custom video player: modal overlay, chaptered timeline, hover-scrub preview, speed menu, picture-in-picture, fullscreen, keyboard shortcuts, and focus trapping.",
  "dependencies": ["motion"],
  "registryDependencies": ["utils"],
  "files": [
    {
      "path": "registry/primitives/effects/video-player/index.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentPropsWithoutRef,\n  type ReactNode,\n  type Ref,\n  type RefCallback,\n  type RefObject,\n  useEffect,\n  useId,\n  useRef,\n} from \"react\";\n\nfunction mergeRefs<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> {\n  return (node) => {\n    for (const ref of refs) {\n      if (!ref) {\n        continue;\n      }\n\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else {\n        (ref as RefObject<T | null>).current = node;\n      }\n    }\n  };\n}\n\nexport interface VideoPlayerChapter {\n  /** Chapter start time, in seconds. */\n  start: number;\n  /** Chapter label shown in the hover-scrub preview pill. */\n  title: string;\n}\n\nexport interface VideoPlayerProps\n  extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  /** Chapter markers segmenting the seek timeline. */\n  chapters?: VideoPlayerChapter[];\n  /**\n   * Content rendered inside the trigger button that opens the player.\n   * @default Video icon + \"Open video player\"\n   */\n  children?: ReactNode;\n  /**\n   * Initial playback speed.\n   * @default 1\n   */\n  defaultSpeed?: number;\n  /**\n   * aria-label for the player dialog.\n   * @default \"Custom video player\"\n   */\n  dialogLabel?: string;\n  /** iOS-optimized fallback source for the main video (iPad/iPhone/touch Mac). */\n  iosSrc?: string;\n  /** Class name merged onto the `.media-01` player root. */\n  playerClassName?: string;\n  /** iOS-optimized fallback source for the hover-scrub preview video. */\n  previewIosSrc?: string;\n  /**\n   * Hosted MP4 URL for the muted hover-scrub preview video.\n   * @default src\n   */\n  previewSrc?: string;\n  ref?: Ref<HTMLDivElement>;\n  /**\n   * Playback speed choices shown in the speed menu.\n   * @default [1, 1.25, 1.5, 1.75, 2]\n   */\n  speedOptions?: number[];\n  /**\n   * Hosted MP4 URL for the main video. Also used for the hover-scrub\n   * preview video unless `previewSrc` is set.\n   */\n  src: string;\n  /** Class name merged onto the trigger button. */\n  triggerClassName?: string;\n}\n\ntype ExtendedVideoElement = HTMLVideoElement & {\n  preservesPitch?: boolean;\n  webkitPreservesPitch?: boolean;\n  webkitEnterFullscreen?: () => void;\n  webkitPresentationMode?: string;\n  webkitSetPresentationMode?: (mode: string) => void;\n  webkitSupportsPresentationMode?: (mode: string) => boolean;\n};\n\nconst DEFAULT_SPEED_OPTIONS = [1, 1.25, 1.5, 1.75, 2];\nconst IOS_USER_AGENT_PATTERN = /iPad|iPhone|iPod/;\n\nfunction formatSpeedLabel(value: number) {\n  return `${Number(value.toFixed(2))}x`;\n}\n\nfunction sanitizeId(value: string) {\n  return value.replace(/[^a-zA-Z0-9_-]/g, \"-\");\n}\n\nconst PLAY_ICON_SHAPES = {\n  play: [\n    [\n      [11, 10],\n      [18, 13.74],\n      [18, 22.28],\n      [11, 26],\n    ],\n    [\n      [18, 13.74],\n      [26, 18],\n      [26, 18],\n      [18, 22.28],\n    ],\n  ],\n  pause: [\n    [\n      [11, 10],\n      [17, 10],\n      [17, 26],\n      [11, 26],\n    ],\n    [\n      [20, 10],\n      [26, 10],\n      [26, 26],\n      [20, 26],\n    ],\n  ],\n} as const;\n\n/**\n * Dependency-free, fully animated MP4 video player: modal overlay, chaptered\n * timeline, hover-scrub preview, speed menu, PiP, fullscreen, keyboard\n * shortcuts, and focus trapping. Styling lives in the injected stylesheet\n * (`VIDEO_PLAYER_STYLES`) rather than Tailwind classes so the component stays\n * a single drop-in file.\n */\nfunction VideoPlayer({\n  chapters,\n  className,\n  defaultSpeed = 1,\n  dialogLabel = \"Custom video player\",\n  children,\n  iosSrc,\n  playerClassName,\n  previewIosSrc,\n  previewSrc,\n  ref,\n  speedOptions = DEFAULT_SPEED_OPTIONS,\n  src,\n  triggerClassName,\n  ...props\n}: VideoPlayerProps) {\n  const reactId = useId();\n  const playerId = `video-player-${sanitizeId(reactId)}`;\n  const containerRef = useRef<HTMLDivElement>(null);\n  const prefersReducedMotion = useReducedMotion();\n\n  useEffect(() => {\n    injectVideoPlayerStyles();\n  }, []);\n\n  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: coordinates media loading, controls, focus, and pointer interactions.\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) {\n      return;\n    }\n\n    const rootRaw = container.querySelector<HTMLElement>(\n      \"[data-media-01-player]\"\n    );\n    if (!rootRaw) {\n      return;\n    }\n    const root = rootRaw;\n\n    const openButtons = [\n      ...container.querySelectorAll<HTMLButtonElement>(\"[data-media-01-open]\"),\n    ];\n    const modal = root;\n    const shellRaw = root.querySelector<HTMLElement>(\"[data-media-shell]\");\n    const stageRaw = root.querySelector<HTMLElement>(\"[data-media-stage]\");\n    const videoRaw =\n      root.querySelector<ExtendedVideoElement>(\"[data-media-video]\");\n    const previewVideoRaw = root.querySelector<HTMLVideoElement>(\n      \"[data-media-preview-video]\"\n    );\n    const playButtonRaw =\n      root.querySelector<HTMLButtonElement>(\"[data-media-play]\");\n    const muteButtonRaw =\n      root.querySelector<HTMLButtonElement>(\"[data-media-mute]\");\n    const volumeInputRaw = root.querySelector<HTMLInputElement>(\n      \"[data-media-volume]\"\n    );\n    const speedRootRaw = root.querySelector<HTMLElement>(\"[data-media-speed]\");\n    const speedButtonRaw = root.querySelector<HTMLButtonElement>(\n      \"[data-media-speed-toggle]\"\n    );\n    const speedMenuRaw = root.querySelector<HTMLElement>(\n      \"[data-media-speed-menu]\"\n    );\n    const speedLabelRaw = root.querySelector<HTMLElement>(\n      \"[data-media-speed-label]\"\n    );\n    const speedOptionEls = [\n      ...root.querySelectorAll<HTMLButtonElement>(\"[data-media-speed-option]\"),\n    ];\n    const pipButtonRaw =\n      root.querySelector<HTMLButtonElement>(\"[data-media-pip]\");\n    const fullscreenButtonRaw = root.querySelector<HTMLButtonElement>(\n      \"[data-media-fullscreen]\"\n    );\n    const centerButtonRaw = root.querySelector<HTMLButtonElement>(\n      \"[data-media-center-toggle]\"\n    );\n    const currentTimeNode = root.querySelector<HTMLElement>(\n      \"[data-media-current]\"\n    );\n    const durationNode = root.querySelector<HTMLElement>(\n      \"[data-media-duration]\"\n    );\n    const timelineRaw = root.querySelector<HTMLElement>(\n      \"[data-media-timeline]\"\n    );\n    const chapterTrackRaw = root.querySelector<HTMLElement>(\n      \"[data-media-chapter-track]\"\n    );\n    const scrubberRaw = root.querySelector<HTMLElement>(\n      \"[data-media-scrubber]\"\n    );\n    const previewCard = root.querySelector<HTMLElement>(\n      \"[data-media-preview-card]\"\n    );\n    const previewTimeNode = root.querySelector<HTMLElement>(\n      \"[data-media-preview-time]\"\n    );\n    const previewTitleNode = root.querySelector<HTMLElement>(\n      \"[data-media-preview-title]\"\n    );\n    const closeButtons = [\n      ...root.querySelectorAll<HTMLButtonElement>(\"[data-media-close]\"),\n    ];\n    const playPath = root.querySelector<SVGPathElement>(\n      \"[data-media-play-path]\"\n    );\n    const roundFilter = root.querySelector<SVGFilterElement>(\n      \"[data-media-round-filter]\"\n    );\n    const pulseRaw = root.querySelector<HTMLElement>(\"[data-media-pulse]\");\n\n    if (!(shellRaw && stageRaw)) {\n      return;\n    }\n    if (!(videoRaw && previewVideoRaw)) {\n      return;\n    }\n    if (!(playButtonRaw && muteButtonRaw && volumeInputRaw)) {\n      return;\n    }\n    if (!(speedRootRaw && speedButtonRaw && speedMenuRaw && speedLabelRaw)) {\n      return;\n    }\n    if (!speedOptionEls.length) {\n      return;\n    }\n    if (!(pipButtonRaw && fullscreenButtonRaw && centerButtonRaw)) {\n      return;\n    }\n    if (!(timelineRaw && chapterTrackRaw && scrubberRaw && pulseRaw)) {\n      return;\n    }\n\n    // Re-bind as definite (non-null) types: the guards above already\n    // verified these at runtime, and closures below need the narrowing to\n    // survive across function-declaration boundaries.\n    const shell = shellRaw;\n    const stage = stageRaw;\n    const video = videoRaw;\n    const previewVideo = previewVideoRaw;\n    const playButton = playButtonRaw;\n    const muteButton = muteButtonRaw;\n    const volumeInput = volumeInputRaw;\n    const speedRoot = speedRootRaw;\n    const speedButton = speedButtonRaw;\n    const speedMenu = speedMenuRaw;\n    const speedLabel = speedLabelRaw;\n    const pipButton = pipButtonRaw;\n    const fullscreenButton = fullscreenButtonRaw;\n    const centerButton = centerButtonRaw;\n    const timeline = timelineRaw;\n    const chapterTrack = chapterTrackRaw;\n    const scrubber = scrubberRaw;\n    const pulse = pulseRaw;\n\n    const controller = new AbortController();\n    const { signal } = controller;\n    let activeBeforeOpen: HTMLElement | null = null;\n    let closeTimer = 0;\n    let closeRevealTimer = 0;\n    let chromeTimer = 0;\n    let previewFrame = 0;\n    let isPointerOverStage = false;\n    let isDragging = false;\n    let activePointerId: number | null = null;\n    let pendingScrubTime: number | null = null;\n    let resumeAfterScrub = false;\n    let lastPointerWasTouch = false;\n    let touchStageHadChrome = false;\n    let lastCenterTouchToggle = 0;\n    let lastVolume = Number(volumeInput.value) || 1;\n    let chapterRanges: { start: number; end: number; title: string }[] = [];\n    let segmentParts: {\n      start: number;\n      end: number;\n      title?: string;\n      element: HTMLElement;\n      fill: HTMLElement;\n    }[] = [];\n    let playIconFrame = 0;\n    let playIconProgress = 0;\n    let playIconTarget: number | null = null;\n    let isMediaLoaded = false;\n    let currentSpeed =\n      parseSpeed(video.getAttribute(\"data-default-speed\")) || 1;\n    const volumeClasses = [\"is-volume-low\", \"is-volume-mid\", \"is-volume-high\"];\n\n    if (roundFilter && playPath) {\n      const filterId = `media-01-round-icon-${sanitizeId(playerId)}`;\n\n      roundFilter.id = filterId;\n      playPath.setAttribute(\"filter\", `url(#${filterId})`);\n    }\n\n    previewVideo.muted = true;\n    previewVideo.playsInline = true;\n\n    function shouldUseIosSource() {\n      return (\n        IOS_USER_AGENT_PATTERN.test(navigator.userAgent) ||\n        (navigator.platform === \"MacIntel\" && navigator.maxTouchPoints > 1)\n      );\n    }\n\n    function getMediaSource(media: HTMLVideoElement) {\n      const source = media.getAttribute(\"data-src\");\n      const iosSource = media.getAttribute(\"data-ios-src\");\n\n      return shouldUseIosSource() && iosSource ? iosSource : source;\n    }\n\n    function loadMedia() {\n      if (isMediaLoaded) {\n        return;\n      }\n      isMediaLoaded = true;\n\n      for (const [media, preload] of [\n        [video, \"auto\"],\n        [previewVideo, \"metadata\"],\n      ] as const) {\n        const source = getMediaSource(media);\n\n        if (source && !media.getAttribute(\"src\")) {\n          media.setAttribute(\"src\", source);\n        }\n\n        media.preload = preload;\n\n        if (source || media.getAttribute(\"src\")) {\n          media.load();\n        }\n      }\n\n      setPlaybackSpeed(currentSpeed, { includeDefault: true });\n    }\n\n    function getDuration() {\n      return Number.isFinite(video.duration) ? video.duration : 0;\n    }\n\n    function clamp(value: number, min: number, max: number) {\n      return Math.min(Math.max(value, min), max);\n    }\n\n    function formatTime(value: number) {\n      if (!Number.isFinite(value)) {\n        return \"0:00\";\n      }\n\n      const total = Math.max(0, Math.floor(value));\n      const hours = Math.floor(total / 3600);\n      const minutes = Math.floor((total % 3600) / 60);\n      const seconds = total % 60;\n      const paddedSeconds = String(seconds).padStart(2, \"0\");\n\n      if (hours > 0) {\n        return `${hours}:${String(minutes).padStart(2, \"0\")}:${paddedSeconds}`;\n      }\n\n      return `${minutes}:${paddedSeconds}`;\n    }\n\n    function parseSpeed(value: string | null) {\n      const speed = Number(value);\n      return Number.isFinite(speed) && speed > 0 ? speed : null;\n    }\n\n    function setPlaybackSpeed(\n      speed: number,\n      { includeDefault = false }: { includeDefault?: boolean } = {}\n    ) {\n      try {\n        if (includeDefault) {\n          video.defaultPlaybackRate = speed;\n        }\n\n        video.preservesPitch = true;\n        video.webkitPreservesPitch = true;\n      } catch {\n        // Ignore unsupported media properties.\n      }\n\n      video.playbackRate = speed;\n    }\n\n    function isVideoPlaying() {\n      return !(video.paused || video.ended);\n    }\n\n    function isTouchInteraction(event: { pointerType?: string } | null = null) {\n      return event?.pointerType === \"touch\" || lastPointerWasTouch;\n    }\n\n    function hideChrome({\n      allowInside = false,\n      clearPreview = false,\n    }: {\n      allowInside?: boolean;\n      clearPreview?: boolean;\n    } = {}) {\n      if (clearPreview) {\n        root.classList.remove(\"has-preview\");\n      }\n\n      if (\n        isDragging ||\n        (!allowInside && isPointerOverStage) ||\n        !speedMenu.hidden ||\n        root.classList.contains(\"has-preview\")\n      ) {\n        return;\n      }\n\n      root.classList.remove(\"is-chrome-visible\");\n    }\n\n    function scheduleChromeHide(\n      delay = 0,\n      options: { allowInside?: boolean; clearPreview?: boolean } = {}\n    ) {\n      clearTimeout(chromeTimer);\n\n      if (delay <= 0) {\n        hideChrome(options);\n        return;\n      }\n\n      chromeTimer = window.setTimeout(() => hideChrome(options), delay);\n    }\n\n    function showChrome(shouldAutoHide = false) {\n      clearTimeout(chromeTimer);\n\n      if (modal.hidden || root.classList.contains(\"is-closing\")) {\n        return;\n      }\n\n      root.classList.add(\"is-chrome-visible\");\n\n      if (shouldAutoHide && isVideoPlaying()) {\n        scheduleChromeHide(4000, {\n          allowInside: true,\n          clearPreview: true,\n        });\n      }\n    }\n\n    function showTouchChrome() {\n      showChrome(false);\n\n      scheduleChromeHide(3000, {\n        allowInside: true,\n        clearPreview: true,\n      });\n    }\n\n    function hideTouchChrome() {\n      clearTimeout(chromeTimer);\n      closeSpeedMenu();\n      root.classList.remove(\"is-chrome-visible\", \"has-preview\");\n      clearHoveredChapter();\n    }\n\n    function beginClosingVisualState() {\n      clearTimeout(chromeTimer);\n      root.classList.add(\"is-closing\");\n      root.classList.remove(\"is-chrome-visible\", \"has-preview\");\n      clearHoveredChapter();\n    }\n\n    function easeOutQuart(progress: number) {\n      return 1 - (1 - progress) ** 4;\n    }\n\n    function getSubpath(points: readonly (readonly [number, number])[]) {\n      const lines = points\n        .map((point, index) => {\n          const command = index === 0 ? \"M\" : \"L\";\n          return `${command} ${point[0].toFixed(2)} ${point[1].toFixed(2)}`;\n        })\n        .join(\" \");\n\n      return `${lines} Z`;\n    }\n\n    function renderPlayIcon(progress: number) {\n      if (!playPath) {\n        return;\n      }\n\n      const subpaths = PLAY_ICON_SHAPES.play.map((playPoints, pathIndex) => {\n        const pausePoints = PLAY_ICON_SHAPES.pause[pathIndex];\n        const points = playPoints.map((point, pointIndex) => {\n          const pausePoint = pausePoints[pointIndex];\n\n          return [\n            point[0] + (pausePoint[0] - point[0]) * progress,\n            point[1] + (pausePoint[1] - point[1]) * progress,\n          ] as const;\n        });\n\n        return getSubpath(points);\n      });\n\n      playPath.setAttribute(\"d\", subpaths.join(\" \"));\n    }\n\n    function morphPlayIcon(isPlaying: boolean) {\n      const target = isPlaying ? 1 : 0;\n\n      if (playIconTarget === target) {\n        return;\n      }\n      playIconTarget = target;\n      cancelAnimationFrame(playIconFrame);\n\n      if (prefersReducedMotion) {\n        playIconProgress = target;\n        renderPlayIcon(playIconProgress);\n        return;\n      }\n\n      const startProgress = playIconProgress;\n      const distance = target - startProgress;\n      const startTime = performance.now();\n      const duration = 240;\n\n      function tick(now: number) {\n        const elapsed = Math.min((now - startTime) / duration, 1);\n        const eased = easeOutQuart(elapsed);\n\n        playIconProgress = startProgress + distance * eased;\n        renderPlayIcon(playIconProgress);\n\n        if (elapsed < 1) {\n          playIconFrame = requestAnimationFrame(tick);\n        } else {\n          playIconProgress = target;\n          renderPlayIcon(playIconProgress);\n        }\n      }\n\n      playIconFrame = requestAnimationFrame(tick);\n    }\n\n    function readChapters() {\n      const duration = getDuration();\n      const data = [...root.querySelectorAll(\"[data-media-chapter]\")]\n        .map((item) => ({\n          start: Number(item.getAttribute(\"data-start\")),\n          title: item.getAttribute(\"data-title\") || \"\",\n        }))\n        .filter((item) => Number.isFinite(item.start) && item.start >= 0)\n        .sort((left, right) => left.start - right.start);\n\n      if (!data.length || data[0].start > 0) {\n        data.unshift({ start: 0, title: \"\" });\n      }\n\n      const deduped = data.filter(\n        (item, index, list) =>\n          index === 0 || item.start !== list[index - 1].start\n      );\n\n      chapterRanges = deduped\n        .filter((item) => !duration || item.start < duration)\n        .map((item, index, list) => ({\n          start: item.start,\n          end: list[index + 1]?.start ?? duration,\n          title: item.title,\n        }))\n        .filter((item) => !duration || item.end > item.start);\n    }\n\n    function renderChapters() {\n      const duration = getDuration();\n\n      readChapters();\n      chapterTrack.textContent = \"\";\n      segmentParts = [];\n\n      if (!(duration && chapterRanges.length)) {\n        const segment = document.createElement(\"span\");\n        const fill = document.createElement(\"span\");\n\n        segment.className = \"chapter\";\n        fill.className = \"fill\";\n        segment.append(fill);\n        chapterTrack.append(segment);\n        segmentParts.push({\n          start: 0,\n          end: duration || 1,\n          element: segment,\n          fill,\n        });\n        updateTimeline();\n        return;\n      }\n\n      for (const chapter of chapterRanges) {\n        const segment = document.createElement(\"span\");\n        const fill = document.createElement(\"span\");\n        const segmentDuration = Math.max(chapter.end - chapter.start, 0.01);\n\n        segment.className = \"chapter\";\n        fill.className = \"fill\";\n        segment.style.setProperty(\"--chapter-grow\", String(segmentDuration));\n        segment.append(fill);\n        chapterTrack.append(segment);\n        segmentParts.push({ ...chapter, element: segment, fill });\n      }\n\n      updateTimeline();\n    }\n\n    function syncVideoRatio() {\n      const width = video.videoWidth;\n      const height = video.videoHeight;\n\n      if (!(width && height)) {\n        return;\n      }\n\n      root.style.setProperty(\"--video-ratio\", `${width} / ${height}`);\n      root.style.setProperty(\"--video-ratio-number\", String(width / height));\n    }\n\n    function hasReadyFrame() {\n      return Boolean(\n        video.readyState >= 2 && video.videoWidth && video.videoHeight\n      );\n    }\n\n    function syncMediaReadyState() {\n      const isReady = hasReadyFrame();\n      const isLoading = !(isReady || modal.hidden);\n\n      if (video.readyState >= 1) {\n        syncVideoRatio();\n      }\n\n      root.classList.toggle(\"is-media-ready\", isReady);\n      root.classList.toggle(\"is-loading\", isLoading);\n      root.setAttribute(\"aria-busy\", String(isLoading));\n\n      return isReady;\n    }\n\n    function getChapterAt(time: number) {\n      for (let index = chapterRanges.length - 1; index >= 0; index -= 1) {\n        if (time >= chapterRanges[index].start) {\n          return chapterRanges[index];\n        }\n      }\n\n      return (\n        chapterRanges[0] || {\n          title: \"\",\n          start: 0,\n          end: getDuration(),\n        }\n      );\n    }\n\n    function getVisualProgressPercent(time: number) {\n      const duration = getDuration();\n      const trackRect = chapterTrack.getBoundingClientRect();\n      const fallback = duration ? (time / duration) * 100 : 0;\n\n      if (!(duration && trackRect.width && segmentParts.length)) {\n        return fallback;\n      }\n\n      const segment =\n        segmentParts.find((part, index) => {\n          const isLast = index === segmentParts.length - 1;\n\n          return time >= part.start && (time < part.end || isLast);\n        }) || segmentParts[0];\n      const segmentRect = segment.element?.getBoundingClientRect();\n\n      if (!segmentRect?.width) {\n        return fallback;\n      }\n\n      const segmentDuration = Math.max(segment.end - segment.start, 0.01);\n      const segmentProgress = clamp(\n        (time - segment.start) / segmentDuration,\n        0,\n        1\n      );\n      const x =\n        segmentRect.left - trackRect.left + segmentRect.width * segmentProgress;\n\n      return clamp((x / trackRect.width) * 100, 0, 100);\n    }\n\n    function updateTimeline(time?: number) {\n      const duration = getDuration();\n      const renderTime =\n        time ??\n        (isDragging && pendingScrubTime !== null\n          ? pendingScrubTime\n          : video.currentTime);\n      const current = clamp(renderTime || 0, 0, duration || 0);\n      const percent = getVisualProgressPercent(current);\n\n      scrubber.style.setProperty(\"--scrubber-left\", `${percent}%`);\n\n      for (const segment of segmentParts) {\n        const segmentDuration = Math.max(segment.end - segment.start, 0.01);\n        const segmentPercent = clamp(\n          ((current - segment.start) / segmentDuration) * 100,\n          0,\n          100\n        );\n\n        segment.fill.style.setProperty(\n          \"--chapter-progress\",\n          `${segmentPercent}%`\n        );\n      }\n\n      if (currentTimeNode) {\n        currentTimeNode.textContent = formatTime(current);\n      }\n      if (durationNode) {\n        durationNode.textContent = formatTime(duration);\n      }\n\n      timeline.setAttribute(\"aria-valuemax\", String(Math.floor(duration || 0)));\n      timeline.setAttribute(\"aria-valuenow\", String(Math.floor(current)));\n      timeline.setAttribute(\n        \"aria-valuetext\",\n        `${formatTime(current)} of ${formatTime(duration)}`\n      );\n    }\n\n    function syncPlayState() {\n      const isPlaying = isVideoPlaying();\n\n      root.classList.toggle(\"is-playing\", isPlaying);\n      morphPlayIcon(isPlaying);\n      playButton.setAttribute(\n        \"aria-label\",\n        isPlaying ? \"Pause video\" : \"Play video\"\n      );\n      centerButton.setAttribute(\n        \"aria-label\",\n        isPlaying ? \"Pause video\" : \"Play video\"\n      );\n\n      if (isPlaying && isPointerOverStage) {\n        showChrome(true);\n      } else {\n        clearTimeout(chromeTimer);\n      }\n    }\n\n    function getVolumeClass(volume: number) {\n      if (volume <= 0.33) {\n        return \"is-volume-low\";\n      }\n      if (volume <= 0.66) {\n        return \"is-volume-mid\";\n      }\n      return \"is-volume-high\";\n    }\n\n    function syncVolumeState() {\n      const volume = video.muted ? 0 : video.volume;\n      const isMuted = video.muted || video.volume === 0;\n      const volumeClass = getVolumeClass(volume);\n\n      if (!video.muted && video.volume > 0) {\n        lastVolume = video.volume;\n      }\n\n      root.classList.remove(...volumeClasses);\n      root.classList.toggle(\"is-muted\", isMuted);\n      if (!isMuted) {\n        root.classList.add(volumeClass);\n      }\n      muteButton.setAttribute(\n        \"aria-label\",\n        isMuted ? \"Unmute video\" : \"Mute video\"\n      );\n      volumeInput.value = String(volume);\n      volumeInput.style.setProperty(\"--volume-percent\", `${volume * 100}%`);\n    }\n\n    function syncFullscreenState() {\n      const isFullscreen = document.fullscreenElement === shell;\n\n      root.classList.toggle(\"is-fullscreen\", isFullscreen);\n      fullscreenButton.setAttribute(\n        \"aria-label\",\n        isFullscreen ? \"Exit fullscreen\" : \"Enter fullscreen\"\n      );\n    }\n\n    function supportsPictureInPicture() {\n      const supportsStandard =\n        document.pictureInPictureEnabled &&\n        typeof video.requestPictureInPicture === \"function\";\n      const supportsWebkit =\n        typeof video.webkitSetPresentationMode === \"function\" &&\n        typeof video.webkitSupportsPresentationMode === \"function\" &&\n        video.webkitSupportsPresentationMode(\"picture-in-picture\");\n\n      return supportsStandard || supportsWebkit;\n    }\n\n    function isPictureInPicture() {\n      return (\n        document.pictureInPictureElement === video ||\n        video.webkitPresentationMode === \"picture-in-picture\"\n      );\n    }\n\n    function syncPictureInPictureState() {\n      const isSupported = supportsPictureInPicture();\n      const isActive = isPictureInPicture();\n\n      pipButton.disabled = !isSupported;\n      pipButton.setAttribute(\"aria-disabled\", String(!isSupported));\n      pipButton.setAttribute(\n        \"aria-label\",\n        isActive ? \"Exit picture in picture\" : \"Enter picture in picture\"\n      );\n      root.classList.toggle(\"is-pip\", isActive);\n    }\n\n    function closeSpeedMenu() {\n      speedMenu.hidden = true;\n      speedButton.setAttribute(\"aria-expanded\", \"false\");\n    }\n\n    function openSpeedMenu() {\n      speedMenu.hidden = false;\n      speedButton.setAttribute(\"aria-expanded\", \"true\");\n      showChrome(false);\n    }\n\n    function toggleSpeedMenu() {\n      if (speedMenu.hidden) {\n        openSpeedMenu();\n      } else {\n        closeSpeedMenu();\n      }\n    }\n\n    function syncSpeedOptions(speed: number) {\n      speedLabel.textContent = formatSpeedLabel(speed);\n      for (const option of speedOptionEls) {\n        const optionSpeed = parseSpeed(option.getAttribute(\"data-speed\"));\n        const isActive = optionSpeed === speed;\n\n        option.classList.toggle(\"is-active\", isActive);\n        option.setAttribute(\"aria-pressed\", String(isActive));\n      }\n    }\n\n    function syncSpeedState(speed: number = video.playbackRate) {\n      const nextSpeed = parseSpeed(String(speed)) || 1;\n\n      currentSpeed = nextSpeed;\n      setPlaybackSpeed(nextSpeed, { includeDefault: true });\n      syncSpeedOptions(nextSpeed);\n    }\n\n    // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: coordinates playback speed state and menu interactions.\n    async function changePlaybackSpeed(speed: number) {\n      const nextSpeed = parseSpeed(String(speed));\n      if (!nextSpeed) {\n        return;\n      }\n\n      const wasPlaying = isVideoPlaying();\n      const duration = getDuration();\n      const currentTime = Number.isFinite(video.currentTime)\n        ? video.currentTime\n        : 0;\n      const restoreTime = duration\n        ? clamp(currentTime + 0.001, 0, Math.max(duration - 0.001, 0))\n        : currentTime;\n\n      if (wasPlaying) {\n        video.pause();\n      }\n\n      currentSpeed = nextSpeed;\n      setPlaybackSpeed(nextSpeed);\n      syncSpeedOptions(nextSpeed);\n\n      if (Number.isFinite(restoreTime)) {\n        try {\n          video.currentTime = restoreTime;\n        } catch {\n          // Ignore seek errors while media metadata is still settling.\n        }\n\n        updateTimeline(currentTime);\n      }\n\n      if (wasPlaying) {\n        try {\n          await video.play();\n        } catch {\n          syncPlayState();\n        }\n      }\n    }\n\n    function clearPulse() {\n      pulse.classList.remove(\"show-play\", \"show-pause\");\n    }\n\n    function showPulse(type: \"play\" | \"pause\") {\n      clearPulse();\n      if (prefersReducedMotion) {\n        return;\n      }\n      // biome-ignore lint/suspicious/noUnusedExpressions: forces a reflow to restart the CSS animation.\n      pulse.offsetWidth;\n      pulse.classList.add(type === \"pause\" ? \"show-pause\" : \"show-play\");\n    }\n\n    async function togglePlay({\n      showFeedback = false,\n    }: {\n      showFeedback?: boolean;\n    } = {}) {\n      const willPlay = video.paused || video.ended;\n\n      if (showFeedback) {\n        showPulse(willPlay ? \"pause\" : \"play\");\n      }\n\n      if (willPlay) {\n        try {\n          await video.play();\n        } catch {\n          syncPlayState();\n        }\n      } else {\n        video.pause();\n      }\n    }\n\n    function onStageClick(event: MouseEvent) {\n      const target = event.target as HTMLElement;\n      const interactive = target.closest?.(\n        \"button, input, .controls, [data-media-timeline]\"\n      );\n\n      if (interactive) {\n        return;\n      }\n\n      const shouldUseTouchChrome = isTouchInteraction(\n        event as unknown as PointerEvent\n      );\n\n      if (shouldUseTouchChrome) {\n        if (touchStageHadChrome) {\n          hideTouchChrome();\n        } else {\n          showTouchChrome();\n        }\n\n        return;\n      }\n\n      showChrome();\n      togglePlay({ showFeedback: true })\n        .catch(syncPlayState)\n        .finally(() => {\n          if (shouldUseTouchChrome) {\n            showTouchChrome();\n          }\n        });\n    }\n\n    async function togglePictureInPicture() {\n      if (!supportsPictureInPicture()) {\n        return;\n      }\n\n      if (document.pictureInPictureElement === video) {\n        await document.exitPictureInPicture();\n        return;\n      }\n\n      if (document.pictureInPictureElement) {\n        await document.exitPictureInPicture();\n      }\n\n      if (\n        document.pictureInPictureEnabled &&\n        typeof video.requestPictureInPicture === \"function\"\n      ) {\n        await video.requestPictureInPicture();\n        return;\n      }\n\n      if (typeof video.webkitSetPresentationMode === \"function\") {\n        video.webkitSetPresentationMode(\n          video.webkitPresentationMode === \"picture-in-picture\"\n            ? \"inline\"\n            : \"picture-in-picture\"\n        );\n      }\n    }\n\n    function toggleMute() {\n      if (video.muted || video.volume === 0) {\n        video.muted = false;\n        video.volume = lastVolume || 0.8;\n      } else {\n        video.muted = true;\n      }\n\n      syncVolumeState();\n    }\n\n    function seekTo(time: number) {\n      const duration = getDuration();\n      if (!duration) {\n        return;\n      }\n\n      video.currentTime = clamp(time, 0, duration);\n      updateTimeline(video.currentTime);\n    }\n\n    function getTimelinePoint(event: { clientX: number }) {\n      const duration = getDuration();\n      const rect = timeline.getBoundingClientRect();\n      const x = clamp(event.clientX - rect.left, 0, rect.width);\n      const percent = rect.width ? x / rect.width : 0;\n\n      return {\n        x,\n        percent,\n        time: duration * percent,\n      };\n    }\n\n    function markPreviewFallback() {\n      root.classList.add(\"is-preview-fallback\");\n    }\n\n    function setHoveredChapter(time: number) {\n      const activeSegment =\n        segmentParts.find((part, index) => {\n          const isLast = index === segmentParts.length - 1;\n          return time >= part.start && (time < part.end || isLast);\n        }) || null;\n\n      for (const part of segmentParts) {\n        part.element.classList.toggle(\"is-hovered\", part === activeSegment);\n      }\n    }\n\n    function clearHoveredChapter() {\n      for (const part of segmentParts) {\n        part.element.classList.remove(\"is-hovered\");\n      }\n    }\n\n    function updatePreviewVideo(time: number) {\n      if (!(previewVideo && Number.isFinite(time))) {\n        return;\n      }\n      if (previewVideo.readyState < 1) {\n        return;\n      }\n\n      cancelAnimationFrame(previewFrame);\n      previewFrame = requestAnimationFrame(() => {\n        try {\n          previewVideo.pause();\n\n          if (Math.abs(previewVideo.currentTime - time) > 0.12) {\n            previewVideo.currentTime = time;\n          }\n        } catch {\n          markPreviewFallback();\n        }\n      });\n    }\n\n    // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: coordinates timeline preview state and pointer interactions.\n    function showPreview(\n      event: { clientX: number; pointerType?: string },\n      time: number\n    ) {\n      if (modal.hidden || root.classList.contains(\"is-closing\")) {\n        return;\n      }\n\n      const duration = getDuration();\n      if (!duration) {\n        return;\n      }\n\n      const timelineRect = timeline.getBoundingClientRect();\n      const wrapRect = timeline.parentElement?.getBoundingClientRect();\n      const previewWidth = previewCard?.offsetWidth || 236;\n      const left = wrapRect\n        ? clamp(\n            event.clientX - wrapRect.left,\n            previewWidth / 2,\n            wrapRect.width - previewWidth / 2\n          )\n        : 0;\n      const chapter = getChapterAt(time);\n      setHoveredChapter(time);\n\n      if (\n        !isTouchInteraction(event as unknown as PointerEvent) &&\n        previewCard\n      ) {\n        previewCard.style.left = `${left}px`;\n      }\n\n      if (previewTimeNode) {\n        previewTimeNode.textContent = formatTime(time);\n      }\n      if (previewTitleNode) {\n        previewTitleNode.textContent = chapter.title;\n      }\n      if (previewCard) {\n        previewCard.classList.toggle(\n          \"has-title\",\n          Boolean(chapter.title.trim())\n        );\n      }\n\n      root.classList.add(\"has-preview\");\n      showChrome(false);\n      if (!isTouchInteraction(event as unknown as PointerEvent)) {\n        updatePreviewVideo(clamp(time, 0, duration));\n      }\n\n      if (timelineRect.width) {\n        timeline.style.setProperty(\n          \"--preview-percent\",\n          `${((event.clientX - timelineRect.left) / timelineRect.width) * 100}%`\n        );\n      }\n    }\n\n    function hidePreview() {\n      root.classList.remove(\"has-preview\");\n      clearHoveredChapter();\n      scheduleChromeHide(300);\n    }\n\n    function setTriggersExpanded(isExpanded: boolean) {\n      for (const button of openButtons) {\n        button.setAttribute(\"aria-expanded\", String(isExpanded));\n        button.classList.toggle(\"is-media-open\", isExpanded);\n      }\n    }\n\n    function openModal(trigger: HTMLElement | null = null) {\n      clearTimeout(closeTimer);\n      clearTimeout(closeRevealTimer);\n      loadMedia();\n      activeBeforeOpen =\n        trigger || (document.activeElement as HTMLElement | null);\n      clearPulse();\n      closeSpeedMenu();\n      modal.hidden = false;\n      root.classList.remove(\"is-close-ready\", \"is-closing\");\n      syncMediaReadyState();\n      setTriggersExpanded(true);\n      video.pause();\n      showChrome(false);\n      syncPlayState();\n\n      requestAnimationFrame(() => {\n        root.classList.add(\"is-player-open\");\n        updateTimeline();\n      });\n\n      closeRevealTimer = window.setTimeout(() => {\n        root.classList.add(\"is-close-ready\");\n      }, 130);\n\n      window.setTimeout(() => {\n        const focusTarget = hasReadyFrame()\n          ? playButton\n          : closeButtons.find((button) => button.classList.contains(\"close\"));\n\n        (focusTarget || playButton).focus({ preventScroll: true });\n      }, 160);\n    }\n\n    async function closeModal() {\n      clearTimeout(closeTimer);\n      clearTimeout(closeRevealTimer);\n      beginClosingVisualState();\n\n      if (document.fullscreenElement === shell) {\n        try {\n          await document.exitFullscreen();\n        } catch {\n          // Ignore fullscreen exit errors; the modal can still close.\n        }\n      }\n\n      video.pause();\n      hidePreview();\n      clearTimeout(chromeTimer);\n      clearPulse();\n      closeSpeedMenu();\n      isPointerOverStage = false;\n      isDragging = false;\n      activePointerId = null;\n      pendingScrubTime = null;\n      resumeAfterScrub = false;\n      root.classList.remove(\n        \"is-player-open\",\n        \"is-close-ready\",\n        \"is-loading\",\n        \"is-scrubbing\",\n        \"is-chrome-visible\"\n      );\n      root.setAttribute(\"aria-busy\", \"false\");\n      setTriggersExpanded(false);\n\n      closeTimer = window.setTimeout(() => {\n        modal.hidden = true;\n        root.classList.remove(\"is-closing\");\n\n        if (activeBeforeOpen && document.contains(activeBeforeOpen)) {\n          activeBeforeOpen.focus({ preventScroll: true });\n        } else if (openButtons[0]) {\n          openButtons[0].focus({ preventScroll: true });\n        }\n      }, 360);\n    }\n\n    function trapFocus(event: KeyboardEvent) {\n      const focusables = [\n        ...modal.querySelectorAll<HTMLElement>(\n          'button:not([disabled]):not([tabindex=\"-1\"]), input:not([disabled]):not([tabindex=\"-1\"]), [tabindex]:not([tabindex=\"-1\"])'\n        ),\n      ].filter((node) => node.offsetParent !== null);\n\n      if (!focusables.length) {\n        return;\n      }\n\n      const first = focusables[0];\n      const last = focusables.at(-1);\n\n      if (event.shiftKey && document.activeElement === first) {\n        event.preventDefault();\n        last?.focus();\n      } else if (!event.shiftKey && document.activeElement === last) {\n        event.preventDefault();\n        first.focus();\n      }\n    }\n\n    function resolveSeekKeyTime(key: string, duration: number) {\n      const seekSmall = 5;\n      const seekLarge = 15;\n\n      if (key === \"ArrowLeft\") {\n        return video.currentTime - seekSmall;\n      }\n      if (key === \"ArrowRight\") {\n        return video.currentTime + seekSmall;\n      }\n      if (key === \"ArrowDown\") {\n        return video.currentTime - seekLarge;\n      }\n      if (key === \"ArrowUp\") {\n        return video.currentTime + seekLarge;\n      }\n      if (key === \"Home\") {\n        return 0;\n      }\n      if (key === \"End\") {\n        return duration;\n      }\n\n      return null;\n    }\n\n    // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: coordinates keyboard shortcuts and player controls.\n    function onDocumentKeydown(event: KeyboardEvent) {\n      if (modal.hidden) {\n        return;\n      }\n\n      const target = event.target;\n      const isEditable =\n        target instanceof HTMLElement &&\n        (target.matches(\"input, textarea, select\") || target.isContentEditable);\n      const isInteractive =\n        target instanceof HTMLElement &&\n        target.closest(\"button, a, input, textarea, select, [role='button']\");\n\n      if (!isEditable && event.key.toLowerCase() === \"m\") {\n        event.preventDefault();\n        event.stopImmediatePropagation();\n        toggleMute();\n        return;\n      }\n\n      if (!(isEditable || isInteractive) && event.key === \" \") {\n        event.preventDefault();\n        event.stopImmediatePropagation();\n        togglePlay({ showFeedback: true })\n          .catch(syncPlayState)\n          .finally(() => {\n            if (isTouchInteraction()) {\n              showTouchChrome();\n            }\n          });\n        return;\n      }\n\n      if (!(isEditable || isInteractive)) {\n        const duration = getDuration();\n        const nextTime = duration\n          ? resolveSeekKeyTime(event.key, duration)\n          : null;\n\n        if (nextTime !== null) {\n          event.preventDefault();\n          event.stopImmediatePropagation();\n          seekTo(nextTime);\n          return;\n        }\n      }\n\n      if (event.key === \"Escape\") {\n        event.preventDefault();\n        event.stopImmediatePropagation();\n\n        if (!speedMenu.hidden) {\n          closeSpeedMenu();\n          speedButton.focus({ preventScroll: true });\n          return;\n        }\n\n        closeModal();\n        return;\n      }\n\n      if (event.key === \"Tab\") {\n        event.stopImmediatePropagation();\n        trapFocus(event);\n      }\n    }\n\n    function onTimelineKeydown(event: KeyboardEvent) {\n      const duration = getDuration();\n      if (!duration) {\n        return;\n      }\n\n      const nextTime = resolveSeekKeyTime(event.key, duration);\n\n      if (nextTime !== null) {\n        event.preventDefault();\n        seekTo(nextTime);\n      }\n    }\n\n    function startScrub(event: PointerEvent) {\n      const duration = getDuration();\n      if (!duration) {\n        return;\n      }\n\n      const point = getTimelinePoint(event);\n\n      activePointerId = event.pointerId;\n      isDragging = true;\n      pendingScrubTime = point.time;\n      resumeAfterScrub = isVideoPlaying();\n\n      if (resumeAfterScrub) {\n        video.pause();\n      }\n\n      root.classList.add(\"is-scrubbing\");\n      showChrome(false);\n      timeline.setPointerCapture?.(event.pointerId);\n      updateTimeline(point.time);\n      showPreview(event, point.time);\n      event.preventDefault();\n    }\n\n    function moveScrub(event: PointerEvent) {\n      const duration = getDuration();\n      if (!duration) {\n        return;\n      }\n      if (event.pointerType === \"touch\" && !isDragging) {\n        return;\n      }\n\n      const point = getTimelinePoint(event);\n\n      if (isDragging) {\n        if (event.pointerId !== activePointerId) {\n          return;\n        }\n\n        pendingScrubTime = point.time;\n        updateTimeline(point.time);\n      }\n\n      showPreview(event, point.time);\n    }\n\n    // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: coordinates pointer scrubbing and playback state.\n    function endScrub(event: PointerEvent) {\n      if (!(isDragging || pendingScrubTime !== null)) {\n        return;\n      }\n      if (activePointerId !== null && event.pointerId !== activePointerId) {\n        return;\n      }\n\n      const commitTime = pendingScrubTime;\n      const shouldResume = resumeAfterScrub;\n\n      isDragging = false;\n      activePointerId = null;\n      pendingScrubTime = null;\n      resumeAfterScrub = false;\n      root.classList.remove(\"is-scrubbing\");\n\n      if (commitTime !== null) {\n        seekTo(commitTime);\n      }\n\n      if (event.pointerType === \"touch\") {\n        root.classList.remove(\"has-preview\");\n        clearHoveredChapter();\n\n        if (shouldResume) {\n          video.play().catch(syncPlayState).finally(showTouchChrome);\n        } else {\n          showTouchChrome();\n        }\n\n        return;\n      }\n\n      if (shouldResume) {\n        video.play().catch(syncPlayState);\n      }\n\n      scheduleChromeHide(300);\n    }\n\n    function onDocumentPointerDown(event: PointerEvent) {\n      if (modal.hidden || speedMenu.hidden) {\n        return;\n      }\n      if (speedRoot.contains(event.target as Node)) {\n        return;\n      }\n\n      closeSpeedMenu();\n    }\n\n    function onWindowResize() {\n      updateTimeline();\n    }\n\n    async function toggleFullscreen() {\n      if (document.fullscreenElement === shell) {\n        await document.exitFullscreen();\n        return;\n      }\n\n      if (shell.requestFullscreen) {\n        await shell.requestFullscreen();\n        return;\n      }\n\n      if (video.webkitEnterFullscreen) {\n        video.webkitEnterFullscreen();\n      }\n    }\n\n    for (const button of openButtons) {\n      button.addEventListener(\"pointerenter\", loadMedia, { signal });\n      button.addEventListener(\"focus\", loadMedia, { signal });\n      button.addEventListener(\"pointerdown\", loadMedia, {\n        signal,\n        passive: true,\n      });\n      button.addEventListener(\"click\", () => openModal(button), { signal });\n    }\n    for (const button of closeButtons) {\n      button.addEventListener(\"pointerdown\", beginClosingVisualState, {\n        signal,\n      });\n      button.addEventListener(\"click\", closeModal, { signal });\n    }\n    playButton.addEventListener(\n      \"click\",\n      () => {\n        const shouldUseTouchChrome = isTouchInteraction();\n\n        if (shouldUseTouchChrome) {\n          showTouchChrome();\n        }\n\n        togglePlay({ showFeedback: true })\n          .catch(syncPlayState)\n          .finally(() => {\n            if (shouldUseTouchChrome) {\n              showTouchChrome();\n            }\n          });\n      },\n      { signal }\n    );\n    centerButton.addEventListener(\n      \"pointerup\",\n      (event) => {\n        if (event.pointerType !== \"touch\") {\n          return;\n        }\n\n        event.preventDefault();\n        event.stopPropagation();\n        lastCenterTouchToggle = performance.now();\n        showTouchChrome();\n\n        togglePlay().catch(syncPlayState).finally(showTouchChrome);\n      },\n      { signal }\n    );\n    centerButton.addEventListener(\n      \"click\",\n      (event) => {\n        event.stopPropagation();\n\n        if (performance.now() - lastCenterTouchToggle < 500) {\n          return;\n        }\n\n        showTouchChrome();\n        togglePlay().catch(syncPlayState).finally(showTouchChrome);\n      },\n      { signal }\n    );\n    centerButton.addEventListener(\n      \"pointerdown\",\n      (event) => event.stopPropagation(),\n      {\n        signal,\n      }\n    );\n    muteButton.addEventListener(\"click\", toggleMute, { signal });\n    speedButton.addEventListener(\"click\", toggleSpeedMenu, { signal });\n    for (const option of speedOptionEls) {\n      option.addEventListener(\n        \"click\",\n        (event) => {\n          const shouldUseTouchChrome = isTouchInteraction(\n            event as PointerEvent\n          );\n          const speed = parseSpeed(option.getAttribute(\"data-speed\"));\n\n          event.preventDefault();\n          event.stopPropagation();\n          closeSpeedMenu();\n\n          if (shouldUseTouchChrome) {\n            showTouchChrome();\n          } else {\n            speedButton.focus({ preventScroll: true });\n          }\n\n          if (speed) {\n            changePlaybackSpeed(speed)\n              .catch(syncPlayState)\n              .finally(() => {\n                if (shouldUseTouchChrome) {\n                  showTouchChrome();\n                }\n              });\n          }\n        },\n        { signal }\n      );\n    }\n    pipButton.addEventListener(\n      \"click\",\n      () => {\n        togglePictureInPicture().catch(syncPictureInPictureState);\n      },\n      { signal }\n    );\n    fullscreenButton.addEventListener(\n      \"click\",\n      () => {\n        toggleFullscreen().catch(syncFullscreenState);\n      },\n      { signal }\n    );\n\n    volumeInput.addEventListener(\n      \"input\",\n      () => {\n        const volume = Number(volumeInput.value);\n\n        video.volume = clamp(Number.isFinite(volume) ? volume : 1, 0, 1);\n        video.muted = video.volume === 0;\n        syncVolumeState();\n      },\n      { signal }\n    );\n\n    video.addEventListener(\n      \"loadedmetadata\",\n      () => {\n        setPlaybackSpeed(currentSpeed, { includeDefault: true });\n        syncVideoRatio();\n        syncMediaReadyState();\n        renderChapters();\n      },\n      { signal }\n    );\n    video.addEventListener(\"loadeddata\", syncMediaReadyState, { signal });\n    video.addEventListener(\"canplay\", syncMediaReadyState, { signal });\n    video.addEventListener(\"durationchange\", renderChapters, { signal });\n    video.addEventListener(\"timeupdate\", () => updateTimeline(), { signal });\n    video.addEventListener(\"play\", syncPlayState, { signal });\n    video.addEventListener(\"pause\", syncPlayState, { signal });\n    video.addEventListener(\"ended\", syncPlayState, { signal });\n    video.addEventListener(\"volumechange\", syncVolumeState, { signal });\n    video.addEventListener(\"enterpictureinpicture\", syncPictureInPictureState, {\n      signal,\n    });\n    video.addEventListener(\"leavepictureinpicture\", syncPictureInPictureState, {\n      signal,\n    });\n    video.addEventListener(\n      \"webkitpresentationmodechanged\",\n      syncPictureInPictureState,\n      {\n        signal,\n      }\n    );\n    pulse.addEventListener(\"animationend\", clearPulse, { signal });\n    previewVideo.addEventListener(\"error\", markPreviewFallback, { signal });\n\n    stage.addEventListener(\n      \"pointerdown\",\n      (event) => {\n        if (event.pointerType !== \"touch\") {\n          return;\n        }\n\n        touchStageHadChrome = root.classList.contains(\"is-chrome-visible\");\n      },\n      { signal }\n    );\n    stage.addEventListener(\n      \"pointerenter\",\n      (event) => {\n        isPointerOverStage = true;\n\n        if (event.pointerType === \"touch\") {\n          showTouchChrome();\n          return;\n        }\n\n        showChrome(isVideoPlaying());\n      },\n      { signal }\n    );\n    stage.addEventListener(\n      \"pointermove\",\n      (event) => {\n        isPointerOverStage = true;\n\n        if (event.pointerType === \"touch\") {\n          showTouchChrome();\n          return;\n        }\n\n        showChrome(isVideoPlaying());\n      },\n      { signal }\n    );\n    stage.addEventListener(\n      \"pointerleave\",\n      (event) => {\n        isPointerOverStage = false;\n\n        if (event.pointerType === \"touch\") {\n          return;\n        }\n\n        hidePreview();\n        hideChrome();\n      },\n      { signal }\n    );\n    stage.addEventListener(\"click\", onStageClick, { signal });\n    root.addEventListener(\n      \"pointerdown\",\n      (event) => {\n        lastPointerWasTouch = event.pointerType === \"touch\";\n\n        if (\n          lastPointerWasTouch &&\n          (event.target as HTMLElement).closest?.(\".controls, .seek\")\n        ) {\n          showTouchChrome();\n        }\n      },\n      { signal, capture: true }\n    );\n    root.addEventListener(\n      \"focusin\",\n      (event) => {\n        if ((event.target as HTMLElement).closest?.(\"[data-media-close]\")) {\n          return;\n        }\n        showChrome(false);\n      },\n      { signal }\n    );\n    root.addEventListener(\n      \"focusout\",\n      (event) => {\n        if (\n          event.relatedTarget instanceof Node &&\n          root.contains(event.relatedTarget)\n        ) {\n          return;\n        }\n\n        scheduleChromeHide();\n      },\n      { signal }\n    );\n\n    timeline.addEventListener(\"pointerdown\", startScrub, { signal });\n    timeline.addEventListener(\"pointermove\", moveScrub, { signal });\n    timeline.addEventListener(\n      \"pointerleave\",\n      () => {\n        if (!isDragging) {\n          hidePreview();\n        }\n      },\n      { signal }\n    );\n    timeline.addEventListener(\"pointerup\", endScrub, { signal });\n    timeline.addEventListener(\"pointercancel\", endScrub, { signal });\n    timeline.addEventListener(\"lostpointercapture\", endScrub, { signal });\n    timeline.addEventListener(\"keydown\", onTimelineKeydown, { signal });\n\n    document.addEventListener(\"keydown\", onDocumentKeydown, {\n      signal,\n      capture: true,\n    });\n    document.addEventListener(\"pointerdown\", onDocumentPointerDown, {\n      signal,\n      capture: true,\n    });\n    document.addEventListener(\"fullscreenchange\", syncFullscreenState, {\n      signal,\n    });\n    window.addEventListener(\"resize\", onWindowResize, { signal });\n\n    syncSpeedState(currentSpeed);\n\n    if (video.readyState >= 1) {\n      syncVideoRatio();\n      renderChapters();\n    } else {\n      updateTimeline();\n    }\n\n    syncMediaReadyState();\n\n    syncPlayState();\n    syncVolumeState();\n    syncFullscreenState();\n    syncPictureInPictureState();\n\n    return () => {\n      clearTimeout(closeTimer);\n      clearTimeout(closeRevealTimer);\n      clearTimeout(chromeTimer);\n      cancelAnimationFrame(previewFrame);\n      cancelAnimationFrame(playIconFrame);\n      controller.abort();\n    };\n  }, [playerId, prefersReducedMotion]);\n\n  const resolvedPreviewSrc = previewSrc ?? src;\n  const resolvedPreviewIosSrc = previewIosSrc ?? iosSrc;\n\n  return (\n    <div\n      className={cn(\"contents\", className)}\n      ref={mergeRefs(containerRef, ref)}\n      {...props}\n    >\n      <button\n        aria-expanded=\"false\"\n        aria-haspopup=\"dialog\"\n        className={cn(\"video-player-open\", triggerClassName)}\n        data-media-01-open={playerId}\n        type=\"button\"\n      >\n        {children ?? (\n          <>\n            <svg\n              aria-hidden=\"true\"\n              fill=\"none\"\n              height=\"24\"\n              stroke=\"currentColor\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth=\"2\"\n              viewBox=\"0 0 24 24\"\n              width=\"24\"\n            >\n              <path d=\"m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5\" />\n              <rect height=\"12\" rx=\"2\" width=\"14\" x=\"2\" y=\"6\" />\n            </svg>\n            Open video player\n          </>\n        )}\n      </button>\n\n      <div\n        aria-label={dialogLabel}\n        aria-modal=\"true\"\n        className={cn(\"media-01\", playerClassName)}\n        data-media-01-player={playerId}\n        data-media-modal=\"\"\n        data-reduced-motion={prefersReducedMotion ? \"\" : undefined}\n        hidden\n        role=\"dialog\"\n      >\n        <div aria-hidden=\"true\" className=\"backdrop\" data-media-close=\"\" />\n\n        <div className=\"frame\">\n          <button\n            aria-label=\"Close video player\"\n            className=\"close\"\n            data-media-close=\"\"\n            type=\"button\"\n          >\n            <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\">\n              <path d=\"M6 6l12 12M18 6 6 18\" />\n            </svg>\n          </button>\n\n          <section className=\"shell\" data-media-shell=\"\">\n            <div className=\"stage\" data-media-stage=\"\">\n              <video\n                className=\"video\"\n                data-default-speed={defaultSpeed}\n                data-ios-src={iosSrc}\n                data-media-video=\"\"\n                data-src={src}\n                playsInline\n                preload=\"none\"\n              >\n                <track kind=\"captions\" />\n              </video>\n\n              <div aria-hidden=\"true\" className=\"loader\">\n                <svg aria-hidden=\"true\" viewBox=\"0 0 19 19\">\n                  <path\n                    d=\"M9.5 2.938v2.625\n                m0 7.875v2.624\n                M2.938 9.5h2.625\n                m7.875 0h2.624\n                M4.86 4.86l1.856 1.856\n                m5.569 5.568 1.856 1.856\n                m-9.28 0 1.855-1.856\n                m5.569-5.568L14.14 4.86\"\n                    strokeLinecap=\"round\"\n                    strokeWidth=\"1.875\"\n                  />\n                </svg>\n              </div>\n\n              <div aria-hidden=\"true\" className=\"shade\" />\n              <div aria-hidden=\"true\" className=\"pulse\" data-media-pulse=\"\">\n                <svg\n                  aria-hidden=\"true\"\n                  className=\"pulse-play\"\n                  viewBox=\"0 0 36 36\"\n                >\n                  <path d=\"M11.45 8.68C9.7 7.78 7.7 9 7.7 10.96V25.04C7.7 27 9.7 28.22 11.45 27.32L23.95 20.86C25.96 19.82 27.9 19.12 27.9 18C27.9 16.88 25.96 16.18 23.95 15.14L11.45 8.68Z\" />\n                </svg>\n                <svg\n                  aria-hidden=\"true\"\n                  className=\"pulse-pause\"\n                  viewBox=\"0 0 36 36\"\n                >\n                  <rect height=\"20\" rx=\"1.75\" width=\"6.5\" x=\"10\" y=\"8\" />\n                  <rect height=\"20\" rx=\"1.75\" width=\"6.5\" x=\"19.5\" y=\"8\" />\n                </svg>\n              </div>\n\n              <button\n                aria-label=\"Play video\"\n                className=\"center-toggle\"\n                data-media-center-toggle=\"\"\n                type=\"button\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  className=\"center-play\"\n                  viewBox=\"0 0 36 36\"\n                >\n                  <path d=\"M11.45 8.68C9.7 7.78 7.7 9 7.7 10.96V25.04C7.7 27 9.7 28.22 11.45 27.32L23.95 20.86C25.96 19.82 27.9 19.12 27.9 18C27.9 16.88 25.96 16.18 23.95 15.14L11.45 8.68Z\" />\n                </svg>\n                <svg\n                  aria-hidden=\"true\"\n                  className=\"center-pause\"\n                  viewBox=\"0 0 36 36\"\n                >\n                  <rect height=\"20\" rx=\"1.75\" width=\"6.5\" x=\"10\" y=\"8\" />\n                  <rect height=\"20\" rx=\"1.75\" width=\"6.5\" x=\"19.5\" y=\"8\" />\n                </svg>\n              </button>\n\n              <div className=\"seek\">\n                <div\n                  aria-hidden=\"true\"\n                  className=\"preview\"\n                  data-media-preview-card=\"\"\n                >\n                  <video\n                    className=\"thumb\"\n                    data-ios-src={resolvedPreviewIosSrc}\n                    data-media-preview-video=\"\"\n                    data-src={resolvedPreviewSrc}\n                    muted\n                    playsInline\n                    preload=\"none\"\n                  >\n                    <track kind=\"captions\" />\n                  </video>\n                  <div className=\"meta\">\n                    <span data-media-preview-time=\"\">0:00</span>\n                    <span data-media-preview-title=\"\">Opening</span>\n                  </div>\n                </div>\n\n                <div\n                  aria-label=\"Seek video\"\n                  aria-valuemax={0}\n                  aria-valuemin={0}\n                  aria-valuenow={0}\n                  aria-valuetext=\"0:00\"\n                  className=\"timeline\"\n                  data-media-timeline=\"\"\n                  role=\"slider\"\n                  tabIndex={0}\n                >\n                  <div className=\"track\" data-media-chapter-track=\"\" />\n                  <div className=\"scrubber\" data-media-scrubber=\"\" />\n                </div>\n              </div>\n\n              <div className=\"controls\">\n                <button\n                  aria-label=\"Play video\"\n                  className=\"control play\"\n                  data-media-play=\"\"\n                  type=\"button\"\n                >\n                  <svg aria-hidden=\"true\" className=\"morph\" viewBox=\"0 0 36 36\">\n                    <defs>\n                      <filter\n                        colorInterpolationFilters=\"sRGB\"\n                        data-media-round-filter=\"\"\n                        height=\"136%\"\n                        width=\"136%\"\n                        x=\"-18%\"\n                        y=\"-18%\"\n                      >\n                        <feGaussianBlur\n                          in=\"SourceGraphic\"\n                          result=\"blur\"\n                          stdDeviation=\"0.7\"\n                        />\n                        <feColorMatrix\n                          in=\"blur\"\n                          mode=\"matrix\"\n                          values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -10\"\n                        />\n                      </filter>\n                    </defs>\n                    <path\n                      d=\"M 11 10 L 18 13.74 L 18 22.28 L 11 26 Z M 18 13.74 L 26 18 L 26 18 L 18 22.28 Z\"\n                      data-media-play-path=\"\"\n                    />\n                  </svg>\n                </button>\n\n                <div className=\"volume\">\n                  <button\n                    aria-label=\"Mute video\"\n                    className=\"control mute\"\n                    data-media-mute=\"\"\n                    type=\"button\"\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"volume-icon volume-low\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z\" />\n                    </svg>\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"volume-icon volume-mid\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z\" />\n                      <path d=\"M16 9a5 5 0 0 1 0 6\" />\n                    </svg>\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"volume-icon volume-high\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z\" />\n                      <path d=\"M16 9a5 5 0 0 1 0 6\" />\n                      <path d=\"M19.364 18.364a9 9 0 0 0 0-12.728\" />\n                    </svg>\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"volume-icon muted\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z\" />\n                      <line x1=\"22\" x2=\"16\" y1=\"9\" y2=\"15\" />\n                      <line x1=\"16\" x2=\"22\" y1=\"9\" y2=\"15\" />\n                    </svg>\n                  </button>\n                  <input\n                    aria-label=\"Volume\"\n                    className=\"range\"\n                    data-media-volume=\"\"\n                    defaultValue=\"1\"\n                    max=\"1\"\n                    min=\"0\"\n                    step=\"0.01\"\n                    type=\"range\"\n                  />\n                </div>\n\n                <div aria-live=\"off\" className=\"time\">\n                  <span data-media-current=\"\">0:00</span>\n                  <span aria-hidden=\"true\">/</span>\n                  <span data-media-duration=\"\">0:00</span>\n                </div>\n\n                <div className=\"speed\" data-media-speed=\"\">\n                  <button\n                    aria-expanded=\"false\"\n                    aria-label=\"Playback speed\"\n                    className=\"control speed-toggle\"\n                    data-media-speed-toggle=\"\"\n                    type=\"button\"\n                  >\n                    <span data-media-speed-label=\"\">\n                      {formatSpeedLabel(defaultSpeed)}\n                    </span>\n                  </button>\n                  <div className=\"speed-menu\" data-media-speed-menu=\"\" hidden>\n                    {speedOptions.map((speed) => (\n                      <button\n                        data-media-speed-option=\"\"\n                        data-speed={speed}\n                        key={speed}\n                        type=\"button\"\n                      >\n                        {formatSpeedLabel(speed)}\n                      </button>\n                    ))}\n                  </div>\n                </div>\n\n                <button\n                  aria-label=\"Enter picture in picture\"\n                  className=\"control pip\"\n                  data-media-pip=\"\"\n                  type=\"button\"\n                >\n                  <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\">\n                    <path d=\"M21 9V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h4\" />\n                    <rect height=\"7\" rx=\"2\" width=\"10\" x=\"12\" y=\"13\" />\n                  </svg>\n                </button>\n\n                <button\n                  aria-label=\"Enter fullscreen\"\n                  className=\"control full\"\n                  data-media-fullscreen=\"\"\n                  type=\"button\"\n                >\n                  <svg aria-hidden=\"true\" className=\"enter\" viewBox=\"0 0 24 24\">\n                    <path d=\"M8 3H5a2 2 0 0 0-2 2v3\" />\n                    <path d=\"M21 8V5a2 2 0 0 0-2-2h-3\" />\n                    <path d=\"M3 16v3a2 2 0 0 0 2 2h3\" />\n                    <path d=\"M16 21h3a2 2 0 0 0 2-2v-3\" />\n                  </svg>\n                  <svg aria-hidden=\"true\" className=\"exit\" viewBox=\"0 0 24 24\">\n                    <path d=\"M9 4v5H4M20 9h-5V4M15 20v-5h5M4 15h5v5\" />\n                  </svg>\n                </button>\n              </div>\n            </div>\n\n            <ul className=\"chapters\" hidden>\n              {(chapters ?? []).map((chapter) => (\n                <li\n                  data-media-chapter=\"\"\n                  data-start={chapter.start}\n                  data-title={chapter.title}\n                  key={chapter.start}\n                />\n              ))}\n            </ul>\n          </section>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nlet stylesInjected = false;\n\nfunction injectVideoPlayerStyles() {\n  if (stylesInjected || typeof document === \"undefined\") {\n    return;\n  }\n  if (document.getElementById(\"sora-video-player-styles\")) {\n    stylesInjected = true;\n    return;\n  }\n\n  const style = document.createElement(\"style\");\n  style.id = \"sora-video-player-styles\";\n  style.textContent = VIDEO_PLAYER_STYLES;\n  document.head.append(style);\n  stylesInjected = true;\n}\n\nconst VIDEO_PLAYER_STYLES = /* css */ `\n@keyframes media-01-pulse {\n  0% {\n    opacity: 0;\n    filter: blur(10px);\n    transform: translate(-50%, -50%) scale(0.72);\n  }\n\n  32% {\n    opacity: 1;\n    filter: blur(0);\n    transform: translate(-50%, -50%) scale(1);\n  }\n\n  72% {\n    opacity: 1;\n    filter: blur(0);\n    transform: translate(-50%, -50%) scale(1);\n  }\n\n  100% {\n    opacity: 0;\n    filter: blur(0);\n    transform: translate(-50%, -50%) scale(1.08);\n  }\n}\n\n@keyframes media-01-spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n[data-media-01-open],\n.media-01 button {\n  border: 0;\n  padding: 0;\n  font: inherit;\n  color: inherit;\n  background: none;\n  cursor: pointer;\n  -webkit-tap-highlight-color: transparent;\n}\n\n[data-media-01-open],\n[data-media-01-open] *,\n.media-01,\n.media-01 *,\n.media-01 *::before,\n.media-01 *::after {\n  box-sizing: border-box;\n}\n\n[data-media-01-open] {\n  --corner-shape: superellipse(1.2);\n  --ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);\n  --media-01-open-text: #ffffff;\n\n  position: relative;\n  height: 48px;\n  padding: 0 24px;\n  display: inline-flex;\n  align-items: center;\n  gap: 12px;\n  border-radius: 999px;\n  corner-shape: var(--corner-shape);\n  background: rgba(53, 53, 53, 0.498);\n  backdrop-filter: blur(10px);\n  color: var(--media-01-open-text);\n  font-size: 14px;\n  font-weight: 450;\n  transition:\n    background 180ms ease-out,\n    scale 260ms var(--ease-out-quart);\n}\n\n[data-media-01-open]:hover {\n  background: rgba(53, 53, 53, 0.598);\n}\n\n[data-media-01-open]:active {\n  scale: 0.96;\n}\n\n[data-media-01-open] svg {\n  width: 16px;\n  height: 16px;\n  fill: none;\n  stroke: currentColor;\n  stroke-width: 2;\n  stroke-linecap: round;\n  stroke-linejoin: round;\n}\n\n.media-01 {\n  --corner-shape: superellipse(1.2);\n  --box-shadow:\n    0px 4px 4px 0px hsla(0, 0%, 0%, 0.039),\n    0px 0px 1px 0px hsla(0, 0%, 0%, 0.62);\n  --ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);\n  --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);\n  --white: #ffffff;\n  --black: #050505;\n  --control: 26px;\n  --icon: 20px;\n  --icon-stroke: 2;\n  --gap: 12px;\n  --inset: 22px;\n  --bottom: 16px;\n  --timeline-bottom: 50px;\n  --time-size: 14px;\n  --time-width: 88px;\n  --volume-open: calc(var(--control) + var(--volume-range) + var(--gap));\n  --volume-range: 54px;\n  --track-height: 4px;\n  --track-gap: 3px;\n\n  position: fixed;\n  inset: 0;\n  z-index: 9999;\n  display: grid;\n  place-items: center;\n  padding: 24px;\n  color: var(--white);\n  -webkit-user-select: none;\n  user-select: none;\n\n  &[hidden] {\n    display: none;\n  }\n\n  &.is-player-open {\n    .backdrop {\n      opacity: 1;\n    }\n\n    .shell {\n      opacity: 1;\n      clip-path: inset(0 round 12px);\n      transform: translateY(0) scale(1);\n    }\n  }\n\n  &.has-preview,\n  &.is-scrubbing {\n    .seek {\n      .preview {\n        opacity: 1;\n        transform: translateX(-50%) translateY(0) scale(1);\n      }\n    }\n  }\n\n  &.is-scrubbing {\n    .video {\n      filter: blur(3px) brightness(0.72);\n      transform: scale(1);\n    }\n  }\n\n  &:not(.is-scrubbing) {\n    .timeline {\n      &:hover,\n      &:focus-visible {\n        .scrubber {\n          width: 14px;\n          height: 14px;\n        }\n      }\n    }\n  }\n\n  &.is-preview-fallback {\n    .seek {\n      .preview {\n        width: 190px;\n\n        .thumb {\n          display: none;\n        }\n      }\n    }\n  }\n\n  &.is-muted {\n    .controls {\n      .volume-icon {\n        display: none;\n      }\n\n      .muted {\n        display: block;\n      }\n    }\n  }\n\n  &.is-volume-low {\n    .controls {\n      .volume-icon {\n        display: none;\n      }\n\n      .volume-low {\n        display: block;\n      }\n    }\n  }\n\n  &.is-volume-mid {\n    .controls {\n      .volume-icon {\n        display: none;\n      }\n\n      .volume-mid {\n        display: block;\n      }\n    }\n  }\n\n  &.is-volume-high {\n    .controls {\n      .volume-icon {\n        display: none;\n      }\n\n      .volume-high {\n        display: block;\n      }\n    }\n  }\n\n  &.is-fullscreen {\n    --bottom: 24px;\n    --timeline-bottom: 58px;\n\n    .controls {\n      .enter {\n        display: none;\n      }\n\n      .exit {\n        display: block;\n      }\n    }\n  }\n\n  &:not(.is-chrome-visible):not(.has-preview):not(.is-scrubbing) {\n    .shade,\n    .seek,\n    .controls {\n      pointer-events: none;\n      opacity: 0;\n    }\n\n    .seek,\n    .controls {\n      transform: translateY(6px);\n    }\n  }\n\n  &.is-loading {\n    .video {\n      opacity: 0;\n    }\n\n    .loader {\n      opacity: 1;\n      transform: translate(-50%, -50%) scale(1);\n    }\n\n    .shade,\n    .seek,\n    .controls {\n      visibility: hidden;\n      pointer-events: none;\n      opacity: 0;\n    }\n  }\n\n  .backdrop {\n    position: absolute;\n    inset: 0;\n    cursor: default;\n    background: rgba(0, 0, 0, 0.22);\n    opacity: 0;\n    backdrop-filter: blur(10px);\n    transition: opacity 360ms var(--ease-out-quart);\n  }\n\n  .frame {\n    position: relative;\n    z-index: 1;\n    width: min(\n      940px,\n      100%,\n      calc((100dvh - 48px) * var(--video-ratio-number, 1.7778))\n    );\n    max-height: calc(100dvh - 48px);\n    aspect-ratio: var(--video-ratio, 16 / 9);\n    transition:\n      width 520ms var(--ease-out-expo),\n      aspect-ratio 520ms var(--ease-out-expo);\n  }\n\n  .shell {\n    position: relative;\n    z-index: 1;\n    width: 100%;\n    height: 100%;\n    overflow: hidden;\n    border: 1px solid rgba(119, 119, 119, 0.31);\n    border-radius: 12px;\n    box-shadow: var(--box-shadow);\n    opacity: 0;\n    clip-path: inset(8% round 12px);\n    transform: translateY(20px) scale(0.95);\n    transition:\n      opacity 420ms var(--ease-out-quart),\n      transform 520ms var(--ease-out-expo),\n      clip-path 520ms var(--ease-out-expo);\n\n    &:fullscreen {\n      width: 100vw;\n      height: 100vh;\n      max-width: none;\n      border-radius: 0;\n      corner-shape: var(--corner-shape);\n      aspect-ratio: auto;\n\n      .stage {\n        height: 100vh;\n      }\n\n      .video {\n        object-fit: contain;\n      }\n    }\n  }\n\n  .stage,\n  .video {\n    width: 100%;\n    height: 100%;\n  }\n\n  .stage {\n    position: relative;\n    background: var(--black);\n  }\n\n  .video {\n    object-fit: cover;\n    opacity: 1;\n    filter: blur(0) brightness(1);\n    transform: scale(1);\n    transition:\n      opacity 280ms ease,\n      filter 180ms ease,\n      transform 180ms ease;\n  }\n\n  .loader {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    z-index: 3;\n    display: grid;\n    place-items: center;\n    width: 42px;\n    height: 42px;\n    color: rgba(255, 255, 255, 0.82);\n    pointer-events: none;\n    opacity: 0;\n    transform: translate(-50%, -50%) scale(0.92);\n    transition:\n      opacity 220ms ease,\n      transform 260ms var(--ease-out-quart);\n\n    svg {\n      width: 24px;\n      height: 24px;\n      fill: none;\n      stroke: currentColor;\n      animation: media-01-spin 900ms linear infinite;\n    }\n  }\n\n  .shade {\n    position: absolute;\n    inset: auto 0 0;\n    height: 52%;\n    pointer-events: none;\n    background: linear-gradient(\n      180deg,\n      transparent 0%,\n      rgba(0, 0, 0, 0.36) 28%,\n      rgba(0, 0, 0, 0.88) 100%\n    );\n    opacity: 1;\n    transition: opacity 260ms ease;\n  }\n\n  .pulse {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    z-index: 2;\n    width: 80px;\n    height: 80px;\n    display: grid;\n    place-items: center;\n    border-radius: 999px;\n    corner-shape: var(--corner-shape);\n    background: rgba(86, 86, 86, 0.28);\n    color: var(--white);\n    opacity: 0;\n    -webkit-backdrop-filter: blur(10px);\n    backdrop-filter: blur(10px);\n    transform: translate(-50%, -50%) scale(0.72);\n    pointer-events: none;\n\n    svg {\n      width: 42px;\n      height: 42px;\n      fill: currentColor;\n    }\n\n    .pulse-play {\n      transform: translateX(2px);\n    }\n\n    .pulse-pause {\n      display: none;\n    }\n\n    &.show-play,\n    &.show-pause {\n      animation: media-01-pulse 1500ms var(--ease-out-expo) both;\n    }\n\n    &.show-pause {\n      .pulse-play {\n        display: none;\n      }\n\n      .pulse-pause {\n        display: block;\n      }\n    }\n  }\n\n  .center-toggle {\n    display: none;\n  }\n\n  .center-pause {\n    display: none;\n  }\n\n  &.is-playing {\n    .center-play {\n      display: none;\n    }\n\n    .center-pause {\n      display: block;\n    }\n  }\n\n  .close {\n    position: absolute;\n    top: -58px;\n    left: 50%;\n    z-index: 2;\n    width: 44px;\n    height: 44px;\n    display: grid;\n    place-items: center;\n    border-radius: 999px;\n    corner-shape: var(--corner-shape);\n    background: rgba(53, 53, 53, 0.498);\n    color: var(--white);\n    backdrop-filter: blur(10px);\n    opacity: 0;\n    pointer-events: none;\n    transform: translate(-50%, 54px);\n    transition:\n      opacity 500ms ease-out,\n      transform 360ms var(--ease-out-expo),\n      background 180ms ease,\n      scale 180ms ease;\n\n    &:hover {\n      background: rgba(37, 37, 37, 0.598);\n    }\n\n    &:active {\n      scale: 0.94;\n    }\n\n    svg {\n      width: 18px;\n      height: auto;\n      fill: none;\n      stroke: currentColor;\n      stroke-width: 2.1;\n      stroke-linecap: round;\n      stroke-linejoin: round;\n    }\n  }\n\n  &.is-player-open.is-close-ready {\n    .close {\n      pointer-events: auto;\n      opacity: 1;\n      transform: translate(-50%, 0);\n    }\n  }\n\n  &.is-closing {\n    .shade,\n    .seek,\n    .controls {\n      visibility: hidden;\n      pointer-events: none;\n      opacity: 0;\n      transition: none;\n    }\n\n    .close {\n      transition:\n        background 180ms ease,\n        scale 180ms ease;\n    }\n  }\n\n  .seek {\n    position: absolute;\n    right: var(--inset);\n    bottom: var(--timeline-bottom);\n    left: var(--inset);\n    z-index: 3;\n    display: flex;\n    align-items: flex-end;\n    height: 34px;\n    opacity: 1;\n    transition:\n      opacity 240ms ease,\n      transform 240ms ease;\n\n    .timeline {\n      position: relative;\n      display: flex;\n      align-items: center;\n      width: 100%;\n      height: 18px;\n      touch-action: none;\n      cursor: pointer;\n\n      .track {\n        position: relative;\n        width: 100%;\n        height: var(--track-height);\n        display: flex;\n        align-items: center;\n        border-radius: 999px;\n        gap: var(--track-gap);\n        corner-shape: var(--corner-shape);\n        transform-origin: center;\n        transition: transform 180ms var(--ease-out-quart);\n\n        .chapter {\n          position: relative;\n          min-width: 10px;\n          height: 100%;\n          flex-basis: 0;\n          flex-grow: var(--chapter-grow, 1);\n          overflow: hidden;\n          corner-shape: var(--corner-shape);\n          background: rgba(255, 255, 255, 0.22);\n          transform-origin: center;\n          transition: transform 180ms var(--ease-out-quart);\n\n          .fill {\n            position: absolute;\n            inset: 0 auto 0 0;\n            width: var(--chapter-progress, 0%);\n            border-radius: inherit;\n            corner-shape: inherit;\n            background: rgba(255, 255, 255, 0.76);\n          }\n        }\n      }\n\n      &:hover,\n      &:focus-visible {\n        .track {\n          transform: scaleY(1.5);\n        }\n\n        .chapter:hover,\n        .chapter.is-hovered {\n          transform: scaleY(1.34);\n        }\n      }\n\n      .scrubber {\n        position: absolute;\n        top: 50%;\n        left: var(--scrubber-left, 0%);\n        width: 10px;\n        height: 10px;\n        border-radius: 999px;\n        corner-shape: var(--corner-shape);\n        background: var(--white);\n        transform: translate(-50%, -50%);\n        pointer-events: none;\n        transition:\n          width 180ms var(--ease-out-quart),\n          height 180ms var(--ease-out-quart);\n      }\n    }\n\n    .preview {\n      position: absolute;\n      bottom: 84px;\n      left: 50%;\n      width: clamp(150px, 22vw, 230px);\n      pointer-events: none;\n      opacity: 0;\n      transform: translateX(-50%) translateY(8px) scale(0.96);\n      transition:\n        opacity 160ms ease,\n        transform 160ms ease;\n\n      .thumb {\n        display: block;\n        width: 100%;\n        aspect-ratio: var(--video-ratio, 16 / 9);\n        border: 0;\n        border-radius: 10px;\n        corner-shape: var(--corner-shape);\n        object-fit: cover;\n        background: #111;\n        box-shadow: var(--box-shadow);\n      }\n\n      .meta {\n        position: absolute;\n        top: calc(100% + 7px);\n        left: 50%;\n        max-width: min(100%, 240px);\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        gap: 8px;\n        padding: 7px 12px 8px;\n        border-radius: 999px;\n        corner-shape: var(--corner-shape);\n        background: rgba(86, 86, 86, 0.28);\n        backdrop-filter: blur(10px);\n        box-shadow: none;\n        color: rgba(255, 255, 255, 0.96);\n        font-size: 14px;\n        font-weight: 450;\n        line-height: 1;\n        filter: drop-shadow(0 1px 8px rgba(0, 0, 0, 0.72));\n        white-space: nowrap;\n        transform: translateX(-50%);\n\n        span:first-child {\n          flex: 0 0 auto;\n        }\n\n        span:last-child {\n          flex: 1 1 auto;\n          min-width: 0;\n          overflow: hidden;\n          text-overflow: ellipsis;\n          color: rgba(255, 255, 255, 0.9);\n          white-space: nowrap;\n        }\n      }\n\n      &:not(.has-title) {\n        .meta {\n          min-width: 0;\n        }\n\n        [data-media-preview-title] {\n          display: none;\n        }\n      }\n    }\n  }\n\n  &.is-scrubbing {\n    .seek {\n      .timeline {\n        .scrubber {\n          width: 17px;\n          height: 17px;\n        }\n      }\n    }\n  }\n\n  .controls {\n    position: absolute;\n    right: var(--inset);\n    bottom: var(--bottom);\n    left: var(--inset);\n    z-index: 4;\n    display: flex;\n    gap: var(--gap);\n    align-items: center;\n    pointer-events: none;\n    opacity: 1;\n    transition:\n      opacity 240ms ease,\n      transform 240ms ease;\n\n    .control,\n    .time,\n    .volume,\n    .speed {\n      pointer-events: auto;\n      background: transparent;\n      box-shadow: none;\n    }\n\n    .control {\n      width: var(--control);\n      height: var(--control);\n      display: grid;\n      place-items: center;\n      flex: 0 0 auto;\n      border-radius: 8px;\n      corner-shape: var(--corner-shape);\n      color: var(--white);\n      opacity: 0.96;\n      filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5));\n      transition:\n        opacity 180ms ease,\n        scale 180ms ease;\n\n      svg {\n        width: var(--icon);\n        height: var(--icon);\n        fill: currentColor;\n      }\n\n      &:hover {\n        opacity: 1;\n      }\n\n      &:active {\n        scale: 0.95;\n      }\n\n      &:disabled {\n        pointer-events: none;\n        opacity: 0.38;\n      }\n    }\n\n    .play {\n      position: relative;\n      width: 30px;\n      overflow: visible;\n\n      svg {\n        position: absolute;\n        inset: 0;\n        width: 31px;\n        height: 31px;\n        margin: auto;\n      }\n    }\n\n    .morph {\n      overflow: visible;\n      fill: currentColor;\n      transform: translateX(1px);\n    }\n\n    .volume-icon,\n    .exit {\n      display: none;\n    }\n\n    .volume-high {\n      display: block;\n    }\n\n    .volume {\n      width: var(--control);\n      height: var(--control);\n      display: flex;\n      align-items: center;\n      flex: 0 0 auto;\n      overflow: hidden;\n      border-radius: 8px;\n      corner-shape: var(--corner-shape);\n      transition: width 260ms var(--ease-out-quart);\n\n      &:hover,\n      &:focus-within {\n        width: var(--volume-open);\n      }\n\n      .control {\n        background: transparent;\n        box-shadow: none;\n        backdrop-filter: none;\n      }\n\n      .range {\n        width: var(--volume-range);\n        height: 20px;\n        margin: 0 0 0 var(--gap);\n        appearance: none;\n        cursor: pointer;\n        background: transparent;\n        opacity: 0;\n        transition: opacity 180ms ease;\n\n        &::-webkit-slider-runnable-track {\n          height: 3px;\n          border: 0;\n          border-radius: 999px;\n          corner-shape: var(--corner-shape);\n          background: linear-gradient(\n            90deg,\n            var(--white) 0 var(--volume-percent, 100%),\n            rgba(255, 255, 255, 0.42) var(--volume-percent, 100%) 100%\n          );\n        }\n\n        &::-webkit-slider-thumb {\n          width: 10px;\n          height: 10px;\n          margin-top: -3.5px;\n          appearance: none;\n          border: 0;\n          border-radius: 999px;\n          corner-shape: var(--corner-shape);\n          background: var(--white);\n        }\n\n        &::-moz-range-track {\n          height: 3px;\n          border: 0;\n          border-radius: 999px;\n          corner-shape: var(--corner-shape);\n          background: linear-gradient(\n            90deg,\n            var(--white) 0 var(--volume-percent, 100%),\n            rgba(255, 255, 255, 0.42) var(--volume-percent, 100%) 100%\n          );\n        }\n\n        &::-moz-range-thumb {\n          width: 10px;\n          height: 10px;\n          border: 0;\n          border-radius: 999px;\n          corner-shape: var(--corner-shape);\n          background: var(--white);\n        }\n      }\n\n      &:hover .range,\n      &:focus-within .range {\n        opacity: 1;\n      }\n    }\n\n    .time {\n      height: var(--control);\n      min-width: var(--time-width);\n      margin-left: -2px;\n      padding: 0 2px;\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      gap: 7px;\n      flex: 0 0 auto;\n      border-radius: 999px;\n      corner-shape: var(--corner-shape);\n      font-size: var(--time-size);\n      font-weight: 450;\n      line-height: 1;\n      font-variant-numeric: tabular-nums;\n      color: var(--white);\n      filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5));\n    }\n\n    .speed {\n      position: relative;\n      margin-left: auto;\n    }\n\n    .pip {\n      margin-right: 2px;\n    }\n\n    .speed-toggle {\n      width: auto;\n      min-width: 42px;\n      padding: 0 4px;\n      font-size: var(--time-size);\n      font-weight: 450;\n      font-variant-numeric: tabular-nums;\n    }\n\n    .speed-menu {\n      position: absolute;\n      right: 0;\n      bottom: calc(100% + 10px);\n      display: flex;\n      align-items: center;\n      gap: 2px;\n      padding: 4px;\n      overflow: hidden;\n      border-radius: 999px;\n      corner-shape: var(--corner-shape);\n      background: rgba(86, 86, 86, 0.28);\n      -webkit-backdrop-filter: blur(24px) saturate(1.35);\n      backdrop-filter: blur(24px) saturate(1.35);\n\n      &[hidden] {\n        display: none;\n      }\n\n      button {\n        height: 24px;\n        padding: 0 8px;\n        border-radius: 999px;\n        corner-shape: var(--corner-shape);\n        color: rgba(255, 255, 255, 0.72);\n        font-size: var(--time-size);\n        font-weight: 450;\n        font-variant-numeric: tabular-nums;\n        white-space: nowrap;\n        transition:\n          background 160ms ease,\n          color 160ms ease;\n\n        &:hover,\n        &.is-active {\n          color: var(--white);\n          background: rgba(255, 255, 255, 0.16);\n        }\n      }\n    }\n\n    .mute svg,\n    .pip svg,\n    .full svg {\n      fill: none;\n      stroke: currentColor;\n      stroke-width: var(--icon-stroke);\n      stroke-linecap: round;\n      stroke-linejoin: round;\n    }\n\n    .pip svg {\n      width: calc(var(--icon) - 1px);\n      height: calc(var(--icon) - 1px);\n      stroke-width: 1.85;\n    }\n\n    .full svg {\n      width: calc(var(--icon) - 2px);\n      height: calc(var(--icon) - 2px);\n    }\n  }\n\n  @media (max-width: 760px) {\n    --control: 30px;\n    --gap: 8px;\n    --icon: 20px;\n    --time-size: 14px;\n    --time-width: 98px;\n    --inset: 16px;\n    --bottom: 14px;\n    --timeline-bottom: 48px;\n\n    padding: 14px;\n\n    .frame {\n      width: min(\n        100%,\n        calc((100dvh - 28px) * var(--video-ratio-number, 1.7778))\n      );\n      max-height: calc(100dvh - 28px);\n      aspect-ratio: var(--video-ratio, 16 / 9);\n    }\n\n    .seek {\n      height: 30px;\n    }\n  }\n\n  @media (hover: none) and (pointer: coarse) {\n    &.is-chrome-visible:not(.is-loading):not(.has-preview):not(.is-scrubbing) {\n      .center-toggle {\n        visibility: visible;\n        pointer-events: auto;\n        opacity: 1;\n      }\n    }\n\n    .pulse {\n      width: 58px;\n      height: 58px;\n\n      svg {\n        width: 30px;\n        height: 30px;\n      }\n    }\n\n    .center-toggle {\n      position: absolute;\n      top: 50%;\n      left: 50%;\n      z-index: 4;\n      width: 58px;\n      height: 58px;\n      display: grid;\n      place-items: center;\n      border-radius: 999px;\n      corner-shape: var(--corner-shape);\n      isolation: isolate;\n      overflow: hidden;\n      background: transparent;\n      color: var(--white);\n      opacity: 0;\n      visibility: hidden;\n      pointer-events: none;\n      transform: translate(-50%, calc(-50% - 18px)) scale(1);\n\n      &::before {\n        position: absolute;\n        inset: 0;\n        z-index: 0;\n        content: \"\";\n        background: rgba(86, 86, 86, 0.42);\n        border-radius: inherit;\n        -webkit-backdrop-filter: blur(14px);\n        backdrop-filter: blur(14px);\n      }\n\n      svg {\n        position: absolute;\n        inset: 0;\n        z-index: 1;\n        width: 30px;\n        height: 30px;\n        margin: auto;\n        fill: currentColor;\n      }\n\n      .center-play {\n        transform: translateX(2px);\n      }\n    }\n\n    .seek {\n      .preview {\n        bottom: clamp(74px, 18vh, 132px);\n        left: 50% !important;\n        width: max-content;\n        max-width: min(560px, calc(100vw - 48px));\n\n        .thumb {\n          display: none;\n        }\n\n        .meta {\n          position: static;\n          gap: 10px;\n          max-width: 100%;\n          padding: 10px 16px 11px;\n          background: rgba(86, 86, 86, 0.28);\n          -webkit-backdrop-filter: blur(18px);\n          backdrop-filter: blur(18px);\n          transform: none;\n        }\n      }\n    }\n\n    .controls {\n      .play,\n      .volume,\n      .pip {\n        display: none;\n      }\n\n      .full {\n        margin-left: 4px;\n      }\n\n      .range {\n        display: none;\n      }\n\n      .volume:hover,\n      .volume:focus-within {\n        width: var(--control);\n      }\n    }\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .media-01,\n  .media-01 * {\n    transition-duration: 0.01ms !important;\n    animation-duration: 0.01ms !important;\n    animation-iteration-count: 1 !important;\n  }\n}\n`;\n\nexport { VideoPlayer };\n",
      "type": "registry:ui",
      "target": "components/sora-ui/effects/video-player.tsx"
    }
  ],
  "meta": {
    "keywords": [
      "video",
      "player",
      "modal",
      "chapters",
      "scrub",
      "preview",
      "picture-in-picture",
      "fullscreen"
    ]
  },
  "type": "registry:ui"
}
