Per-platform walkthrough/demo video widget in two placements. Renders a collapsed card (hover preview, transport controls) that opens a pure-theater dialog — a centered 16:9 stage on a dimmed backdrop. The host supplies the video data; the widget owns every playback state handoff between card, hover preview, and theater. ## Key Components ### Exports - **`FloatingWalkthroughVideo`** — The widget pinned into a bottom corner over the page (cookie dismissal, appear delay, footer fade). Controlled (`open` + `onOpenChange`) or uncontrolled (`defaultOpen`) theater state. - **`InlineWalkthroughVideo`** — The same widget as an in-flow block filling its container at 16:9, for a page that gives the video a slot of its own. No pinning, z-layer, shadow, appear delay, footer fade or dismissal. - **`WalkthroughVideoData`** — Wire-shape contract: `mainVideoUrl` / `youtubeUrl` (YouTube wins when both are set), `posterUrl`, `captionsUrl` (relative `/api/captions/...` path), `title`, `presenterAvatarUrl`, `position` (`'left' | 'right'` bottom-corner pin), `id` (drives id-match cookie dismissal — a new video re-shows a dismissed card). - **`FloatingWalkthroughVideoProps` / `InlineWalkthroughVideoProps`** — A shared base plus, for the floating one only, the overlay knobs (`appearDelayMs`, `dismissal`, `hideNearSelector`, `pathname`). They are absent from the inline props on purpose: each is inert in-flow, so "floating only" is a compile error rather than a comment. See behaviors below for the non-obvious ones. Both components are one-line presets over an internal `` engine, which is NOT exported — placement is a constant at every call site, so the public API is two components that say what they are instead of one that takes a mode. Everything below the wrapper (player mode machine, audio invariant, corner map, dismissal) is shared, because forking that is how the two placements would drift. ### Key Behaviors - **Appear gate**: the collapsed card mounts after `appearDelayMs` (default 3000 ms floating, 0 inline) + an idle callback, and never renders when dismissed. The THEATER is deliberately NOT gated — a host `open={true}` or `defaultOpen` can force it before the card appears or after dismissal, which is what makes deep links work. - **`deepLinkParam`** — query-param NAME (defaults to `WALKTHROUGH_OPEN_QUERY_PARAM`, `'walkthrough'`, from `utils/walkthrough-deep-link.ts`; pass `''` to disable): when present in `window.location.search` at first client render, the theater opens immediately and PAUSED — `defaultOpen + defaultOpenPaused` decided inside the component, so hosts don't defer their first render to read the URL. Presence-based, read once (deep links arrive by full page load). SSR-safe: the server renders closed and the theater lives in a portal. URL minters (emails, redirects) use the sibling `withWalkthroughOpenParam(url)`. - **`defaultOpenPaused`** — with `defaultOpen`, the initial theater session starts PAUSED instead of autoplaying. For deep links (e.g. the hub's `?walkthrough=1`), where the open has no user gesture and unrequested audio/motion would be hostile. File player: both autoplay flags suppressed (MuxPlayer mounts paused on its poster). YouTube: `autoActivate` suppressed instead — the activated iframe hardcodes `autoplay=1`, so the click-to-play facade IS the paused presentation. The paused session is exactly the initial `defaultOpen` theater: it ends when the NEXT (gesture- or host-driven) open mounts a fresh theater, which autoplays as usual. It is deliberately NOT cleared on close — the closing player stays mounted through the dialog's exit animation, and a prop flip there would restart playback. - **Resume handoff**: closing the theater mid-play hands the timestamp + mute intent back to the card, which continues playback inline; reopening seeds the theater from the live card position. A finished video reopens at 0, not its own last frame. - **Dismissal**: cookie-based, id-matched (mirrors the announcement bar). `dismissal={false}` removes the X entirely; `dismissal.storageKey` is the per-platform cookie name (see `walkthroughDismissCookieName`). - **`pathname`**: route identity from the host — the lib can't observe navigation. Changing it re-queries the footer IntersectionObserver target (`hideNearSelector`, default `footer`) that fades the card out over the footer. ## Usage Example ```typescript // Host layout (hub: components/shared/global-walkthrough-video-client.tsx) ``` > **Deep-link contract:** prefer `deepLinkParam` — the component reads the URL itself at the right moment. If a host drives `defaultOpen` manually instead, it is a load-time value: decide it BEFORE the component's first render, not by flipping a prop afterwards. ## Source [`walkthrough-video.tsx`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/openframe-frontend-core/src/components/features/walkthrough-video.tsx)