> ## Documentation Index
> Fetch the complete documentation index at: https://hyperframes.heygen.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Browser Device Stage

> A generic app surface in token-native device chrome (browser, window, or phone). The screen is a slot for caller img/video/HTML with a token-styled skeleton default; one settle entrance, a readable hold, and an optional screen swap.

export const InstallCommand = ({command}) => {
  const [copied, setCopied] = React.useState(false);
  const copy = async () => {
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(command);
      } else {
        const previous = document.activeElement;
        const scratch = document.createElement("textarea");
        scratch.value = command;
        scratch.setAttribute("readonly", "");
        scratch.style.position = "fixed";
        scratch.style.opacity = "0";
        document.body.appendChild(scratch);
        scratch.select();
        document.execCommand("copy");
        document.body.removeChild(scratch);
        previous?.focus?.();
      }
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch {}
  };
  return <div className="hf-install-command not-prose my-4 flex items-stretch overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
      <code className="flex-1 overflow-x-auto whitespace-nowrap border-r border-zinc-200 px-4 py-3 font-mono text-sm text-zinc-800 dark:border-zinc-800 dark:text-zinc-100">
        {command}
      </code>
      <button type="button" onClick={copy} data-copied={copied ? "true" : "false"} aria-label={`Copy ${command} to the clipboard`} className="hf-install-copy">
        <svg className="hf-install-copy-clipboard" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M14.25 5.25H7.25C6.14543 5.25 5.25 6.14543 5.25 7.25V14.25C5.25 15.3546 6.14543 16.25 7.25 16.25H14.25C15.3546 16.25 16.25 15.3546 16.25 14.25V7.25C16.25 6.14543 15.3546 5.25 14.25 5.25Z" />
          <path d="M2.80103 11.998L1.77203 5.07397C1.61003 3.98097 2.36403 2.96397 3.45603 2.80197L10.38 1.77297C11.313 1.63397 12.19 2.16297 12.528 3.00097" />
        </svg>
        <svg className="hf-install-copy-check" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M2.75 9.5L6.5 13.25L15.25 4.5" />
        </svg>
      </button>
      <span className="hf-install-copy-status" role="status" aria-live="polite">
        {copied ? "Copied" : ""}
      </span>
    </div>;
};

<iframe className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800" title="browser-device-stage preview" loading="lazy" srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/components/browser-device-stage.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/browser-device-stage.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add browser-device-stage" />

That writes one file: `compositions/components/browser-device-stage.html`.

## Paste it into your composition

Open `compositions/components/browser-device-stage.html` and copy what is inside into your own composition.

A component has no size or duration of its own. It takes both from the composition
you paste it into.

## Variables

Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:

| Variable  | Default           | Accepts                      | What it does                                                                |
| --------- | ----------------- | ---------------------------- | --------------------------------------------------------------------------- |
| `chrome`  | `browser`         | `browser`, `window`, `phone` | Device frame drawn around the screen slot.                                  |
| `title`   | `app.example.com` | string                       | Address pill text (browser) or title bar text (window).                     |
| `swap_at` | `0`               | 0s to 30s, step 0.1s         | Seconds from mount start to swap to the second screen. 0 disables the swap. |
| `accent`  | `green`           | `green`, `blue`, `violet`    | Token the skeleton accent elements ride.                                    |
| `exit`    | `none`            | `none`, `fade`, `up`         | Optional departure. None holds the final frame.                             |

Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:

```html wrap theme={null}
<div
  data-composition-id="browser-device-stage"
  data-composition-src="compositions/components/browser-device-stage.html"
  data-variable-values='{"chrome":"browser","title":"app.example.com","swap_at":0,"accent":"green","exit":"none"}'
></div>
```

## Source

<Accordion title={`browser-device-stage.html`}>
  ```html theme={null}
  <!doctype html>
  <!--
    browser-device-stage -- HyperFrames video primitive (product demo / stage)

    A generic app surface presented in token-native device chrome. One settle
    entrance (rise + soft scale), then a slow readable hold; optionally the
    screen swaps to a second screen at swap_at via a brief fade through the
    bare stage (never two screens at partial opacity). The chrome is drawn
    entirely from contract tokens: no traffic-light reds, no fake product
    branding, hairline borders and surface mixes only.

    Chrome (enum):
      - browser: toolbar with three muted dots and a centered address pill
        showing `title` in mono type.
      - window: slim title bar with three muted dots and `title` centered in
        body type.
      - phone: portrait device, dark notch pill, bezel inset, centered.

    The screen is a SLOT. Callers supply content by placing inert templates
    anywhere in the HOST page (templates never render, and the runtime wipes
    the host clip's own children on mount, so the slot lives at document
    level):

      <template data-slot="browser-device-stage-screen"> ... </template>
      <template data-slot="browser-device-stage-screen-b"> ... </template>

    Slot content may be an <img>, a muted <video>, or arbitrary HTML; direct
    img/video children are stretched to cover the screen. When no screen slot
    exists, the primitive renders a tasteful token-styled skeleton app (header
    bar, sidebar, content cards; hairline borders, surface tokens, one accent
    element). With swap_at > 0 and no screen-b slot, the second screen is a
    rearranged skeleton state so the swap still reads.

    Variables (declared in data-composition-variables below):
      - chrome ("browser" | "window" | "phone", default "browser")
      - title (string, default "app.example.com"): address pill / title bar text.
      - swap_at (number seconds, default 0): swap to the second screen at
        this offset from mount start; 0 disables. Clamped inside the hold so
        the swap never fights the entrance or the exit.
      - accent ("green" | "blue" | "violet", default "green"): green rides
        --brand, blue rides --accent, violet rides --accent-2.
      - exit ("none" | "fade" | "up", default "none"): frame roots own
        transitions; the default holds the final frame to the last pixel.

    Envelope, fixed IN and OUT with elastic HOLD only (never timeScale):
      IN_BASE  = 0.90s  one settle: rise + soft scale + fade, smooth ease-out
      HOLD     = max(0, D - IN - OUT); a barely-there vertical drift, then
                 authored stillness
      OUT_BASE = 0.50s  only when exit != none
      If D < IN_BASE + OUT_BASE, IN and OUT compress together.

    Mount contract: the runtime clones only this template. #root fills the
    host box (no data-width/data-height, container-type: size, cqmin units),
    is styled via #root only, and registers one paused timeline under the
    LITERAL "browser-device-stage" key. All state is set with explicit
    endpoints (gsap.set + fromTo) so any seek order lands identical frames.
  -->
  <html
    lang="en"
    data-composition-id="browser-device-stage"
    data-composition-duration="5"
    data-composition-variables='[
      { "id": "chrome", "type": "enum", "role": "content", "label": "Chrome", "description": "Device frame drawn around the screen slot.", "default": "browser", "options": [{ "value": "browser", "label": "Browser" }, { "value": "window", "label": "Window" }, { "value": "phone", "label": "Phone" }] },
      { "id": "title", "type": "string", "role": "content", "label": "Title", "description": "Address pill text (browser) or title bar text (window).", "default": "app.example.com" },
      { "id": "swap_at", "type": "number", "role": "content", "label": "Swap at", "description": "Seconds from mount start to swap to the second screen. 0 disables the swap.", "default": 0, "min": 0, "max": 30, "step": 0.1, "unit": "s" },
      { "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Token the skeleton accent elements ride.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
      { "id": "exit", "type": "enum", "role": "style", "label": "Exit", "description": "Optional departure. None holds the final frame.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
    ]'
  >
    <head>
      <meta charset="UTF-8" />
      <title>Browser Device Stage</title>
    </head>
    <body>
      <template>
        <div id="root" data-composition-id="browser-device-stage" data-duration="5" data-fps="30">
          <style>
            *,
            *::before,
            *::after {
              box-sizing: border-box;
            }

            #root {
              position: absolute;
              inset: 0;
              overflow: hidden;
              container-type: size;
              isolation: isolate;
              color: var(--fg, #f8fafc);
              font-family: var(--font-body, ui-sans-serif, system-ui, sans-serif);
              pointer-events: none;
            }

            .bds-clip {
              position: absolute;
              inset: 0;
              display: grid;
              place-items: center;
              overflow: hidden;
              background: var(--bg, transparent);
            }

            .bds-stage {
              will-change: transform, opacity;
            }

            .bds-device {
              display: grid;
              grid-template-rows: auto minmax(0, 1fr);
              overflow: hidden;
              border: 0.16cqmin solid color-mix(in srgb, var(--border, #475569) 85%, transparent);
              border-radius: calc(var(--radius, 2cqmin) * 1.3);
              background: color-mix(in srgb, var(--surface, #0b1016) 96%, var(--bds-accent));
              box-shadow: 0 3cqh 9cqh color-mix(in srgb, var(--bg, #000000) 45%, transparent);
            }

            .bds-browser,
            .bds-window {
              width: min(82cqw, 126cqh);
              aspect-ratio: 16 / 10;
            }

            .bds-phone {
              position: relative;
              grid-template-rows: minmax(0, 1fr);
              height: min(86cqh, 168cqw);
              aspect-ratio: 9 / 19;
              padding: 1.7cqmin;
              border-radius: 7cqmin;
            }

            /* chrome bar (browser toolbar / window title bar) */
            .bds-bar {
              display: flex;
              align-items: center;
              gap: var(--space-2, 2.2cqmin);
              padding: 1.5cqmin 2.4cqmin;
              border-bottom: 0.14cqmin solid
                color-mix(in srgb, var(--border, #475569) 60%, transparent);
              background: color-mix(in srgb, var(--surface, #0b1016) 80%, var(--bg, #05070c));
            }

            .bds-phone .bds-bar {
              display: none;
            }

            .bds-dots,
            .bds-bar::after {
              display: flex;
              flex: 0 0 5.5cqmin;
              gap: 0.95cqmin;
            }

            .bds-bar::after {
              content: "";
            }

            .bds-dots i {
              width: 1.15cqmin;
              height: 1.15cqmin;
              border-radius: 50%;
              background: color-mix(in srgb, var(--fg, #f8fafc) 26%, transparent);
            }

            .bds-pill {
              display: block;
              min-width: 36%;
              max-width: 62%;
              margin: 0 auto;
              padding: 0.75cqmin 2.6cqmin;
              overflow: hidden;
              border: 0.14cqmin solid color-mix(in srgb, var(--border, #475569) 50%, transparent);
              border-radius: 999px;
              background: color-mix(in srgb, var(--bg, #05070c) 55%, var(--surface, #0b1016));
              color: var(--muted, #8b93a3);
              font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
              font-size: 2cqmin;
              line-height: 1.5;
              white-space: nowrap;
              text-align: center;
              text-overflow: ellipsis;
            }

            .bds-bartitle {
              display: block;
              margin: 0 auto;
              padding: 0.4cqmin 0;
              overflow: hidden;
              color: color-mix(in srgb, var(--fg, #f8fafc) 78%, transparent);
              font-size: 2.15cqmin;
              font-weight: 600;
              letter-spacing: 0.02em;
              line-height: 1.5;
              white-space: nowrap;
              text-overflow: ellipsis;
            }

            .bds-browser .bds-bartitle,
            .bds-window .bds-pill {
              display: none;
            }

            .bds-notch {
              display: none;
            }

            .bds-phone .bds-notch {
              display: block;
              position: absolute;
              z-index: 2;
              top: 3cqmin;
              left: 50%;
              width: 26%;
              height: 2.2cqmin;
              transform: translateX(-50%);
              border-radius: 999px;
              background: color-mix(in srgb, var(--bg, #05070c) 72%, #000000);
            }

            /* screen area: stacked slot layers */
            .bds-screen {
              position: relative;
              overflow: hidden;
              background: color-mix(in srgb, var(--surface, #0b1016) 86%, var(--bg, #05070c));
            }

            .bds-phone .bds-screen {
              border-radius: 5.4cqmin;
            }

            .bds-screen-a,
            .bds-screen-b {
              position: absolute;
              inset: 0;
              overflow: hidden;
            }

            .bds-screen-a > img,
            .bds-screen-a > video,
            .bds-screen-b > img,
            .bds-screen-b > video {
              display: block;
              width: 100%;
              height: 100%;
              object-fit: cover;
            }

            /* default skeleton screen: pure token furniture, no branding */
            .bds-skel {
              position: absolute;
              inset: 0;
              display: grid;
              grid-template-rows: auto minmax(0, 1fr);
            }

            .bds-skel-top {
              display: flex;
              align-items: center;
              gap: var(--space-1, 1.6cqmin);
              padding: 1.8cqmin 2.6cqmin;
              border-bottom: 0.14cqmin solid
                color-mix(in srgb, var(--border, #475569) 55%, transparent);
            }

            .bds-phone .bds-skel-top {
              padding-top: 6.4cqmin;
            }

            .bds-skel-mark {
              width: 2.6cqmin;
              height: 2.6cqmin;
              border-radius: 0.8cqmin;
              background: color-mix(in srgb, var(--bds-accent) 74%, var(--surface, #0b1016));
            }

            .bds-skel-line {
              height: 1.25cqmin;
              border-radius: 999px;
              background: color-mix(in srgb, var(--fg, #f8fafc) 13%, transparent);
            }

            .bds-skel-top .bds-skel-line:nth-of-type(1) {
              width: 9cqmin;
            }

            .bds-skel-top .bds-skel-line:nth-of-type(2) {
              width: 6.5cqmin;
            }

            .bds-skel-spring {
              flex: 1;
            }

            .bds-skel-chip {
              width: 8.5cqmin;
              height: 2.9cqmin;
              border: 0.14cqmin solid color-mix(in srgb, var(--bds-accent) 55%, transparent);
              border-radius: 999px;
              background: color-mix(in srgb, var(--bds-accent) 28%, transparent);
            }

            .bds-skel-avatar {
              width: 2.8cqmin;
              height: 2.8cqmin;
              border-radius: 50%;
              background: color-mix(in srgb, var(--fg, #f8fafc) 18%, transparent);
            }

            .bds-skel-body {
              display: grid;
              grid-template-columns: 24% minmax(0, 1fr);
              min-height: 0;
            }

            .bds-phone .bds-skel-body {
              grid-template-columns: minmax(0, 1fr);
            }

            .bds-skel-side {
              display: flex;
              flex-direction: column;
              gap: 2cqmin;
              padding: 2.6cqmin 2.6cqmin;
              border-right: 0.14cqmin solid
                color-mix(in srgb, var(--border, #475569) 55%, transparent);
            }

            .bds-phone .bds-skel-side {
              display: none;
            }

            .bds-skel-side .bds-skel-line:nth-child(1) {
              width: 78%;
              background: color-mix(in srgb, var(--fg, #f8fafc) 22%, transparent);
            }

            .bds-skel-side .bds-skel-line:nth-child(2) {
              width: 56%;
            }

            .bds-skel-side .bds-skel-line:nth-child(3) {
              width: 66%;
            }

            .bds-skel-side .bds-skel-line:nth-child(4) {
              width: 44%;
            }

            .bds-skel-side .bds-skel-line:nth-child(5) {
              width: 60%;
            }

            .bds-skel-main {
              display: flex;
              flex-direction: column;
              gap: var(--space-2, 1.8cqmin);
              min-height: 0;
              padding: var(--space-3, 2.8cqmin);
            }

            .bds-skel-heading {
              width: 34%;
              height: 2.5cqmin;
              border-radius: 999px;
              background: color-mix(in srgb, var(--fg, #f8fafc) 21%, transparent);
            }

            .bds-skel-sub {
              width: 52%;
              height: 1.25cqmin;
              border-radius: 999px;
              background: color-mix(in srgb, var(--fg, #f8fafc) 9%, transparent);
            }

            .bds-skel-cards {
              display: grid;
              flex: 1;
              grid-template-columns: repeat(3, minmax(0, 1fr));
              gap: 2.1cqmin;
              min-height: 0;
              margin-top: 0.8cqmin;
            }

            .bds-phone .bds-skel-cards {
              grid-template-columns: minmax(0, 1fr);
            }

            .bds-skel-card {
              display: flex;
              flex-direction: column;
              gap: 1.5cqmin;
              min-height: 0;
              padding: 2.1cqmin;
              border: 0.14cqmin solid color-mix(in srgb, var(--border, #475569) 60%, transparent);
              border-radius: var(--radius, 1.6cqmin);
              background: color-mix(in srgb, var(--bg, #05070c) 30%, transparent);
            }

            .bds-skel-card .bds-skel-line {
              width: 55%;
            }

            .bds-skel-block {
              flex: 1;
              min-height: 3cqmin;
              border-radius: calc(var(--radius, 1.6cqmin) * 0.6);
              background: color-mix(in srgb, var(--fg, #f8fafc) 7%, transparent);
            }

            .bds-skel-card:nth-child(1) .bds-skel-block {
              background: color-mix(in srgb, var(--bds-accent) 17%, transparent);
            }

            /* skeleton state B: rearranged furniture so the swap reads */
            .bds-skel-b .bds-skel-heading {
              width: 24%;
            }

            .bds-skel-b .bds-skel-sub {
              width: 38%;
            }

            .bds-skel-b .bds-skel-cards {
              grid-template-columns: 2fr 1fr;
            }

            .bds-phone .bds-skel-b .bds-skel-cards {
              grid-template-columns: minmax(0, 1fr);
            }

            .bds-skel-b .bds-skel-card:nth-child(3) {
              grid-column: 1 / -1;
            }

            .bds-skel-b .bds-skel-card:nth-child(1) .bds-skel-block {
              background: color-mix(in srgb, var(--fg, #f8fafc) 7%, transparent);
            }

            .bds-skel-b .bds-skel-card:nth-child(2) .bds-skel-block {
              background: color-mix(in srgb, var(--bds-accent) 17%, transparent);
            }
          </style>

          <div
            id="browser-device-stage-clip"
            class="bds-clip clip"
            data-start="0"
            data-duration="5"
            data-track-index="0"
          >
            <div class="bds-stage">
              <div class="bds-device" role="img" aria-label="App screen in device chrome">
                <div class="bds-bar">
                  <span class="bds-dots" aria-hidden="true"><i></i><i></i><i></i></span>
                  <span class="bds-pill"></span>
                  <span class="bds-bartitle"></span>
                </div>
                <span class="bds-notch" aria-hidden="true"></span>
                <div class="bds-screen">
                  <div class="bds-screen-a">
                    <div class="bds-skel" aria-hidden="true">
                      <div class="bds-skel-top">
                        <span class="bds-skel-mark"></span>
                        <span class="bds-skel-line"></span>
                        <span class="bds-skel-line"></span>
                        <span class="bds-skel-spring"></span>
                        <span class="bds-skel-chip"></span>
                        <span class="bds-skel-avatar"></span>
                      </div>
                      <div class="bds-skel-body">
                        <div class="bds-skel-side">
                          <span class="bds-skel-line"></span>
                          <span class="bds-skel-line"></span>
                          <span class="bds-skel-line"></span>
                          <span class="bds-skel-line"></span>
                          <span class="bds-skel-line"></span>
                        </div>
                        <div class="bds-skel-main">
                          <span class="bds-skel-heading"></span>
                          <span class="bds-skel-sub"></span>
                          <div class="bds-skel-cards">
                            <div class="bds-skel-card">
                              <span class="bds-skel-line"></span>
                              <span class="bds-skel-block"></span>
                            </div>
                            <div class="bds-skel-card">
                              <span class="bds-skel-line"></span>
                              <span class="bds-skel-block"></span>
                            </div>
                            <div class="bds-skel-card">
                              <span class="bds-skel-line"></span>
                              <span class="bds-skel-block"></span>
                            </div>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
          <script>
            (function () {
              "use strict";

              var root = document.getElementById("root");
              var stage = root.querySelector(".bds-stage");
              var device = root.querySelector(".bds-device");
              var screen = root.querySelector(".bds-screen");
              var screenA = root.querySelector(".bds-screen-a");
              var vars =
                window.__hyperframes && window.__hyperframes.getVariables
                  ? window.__hyperframes.getVariables()
                  : {};

              var chromeKind =
                vars.chrome === "window" || vars.chrome === "phone" ? vars.chrome : "browser";
              var title = vars.title == null ? "app.example.com" : String(vars.title);
              var swapAt = parseFloat(vars.swap_at);
              if (!isFinite(swapAt) || swapAt <= 0) swapAt = 0;
              // Each enum choice routes to a DIFFERENT contract token so the
              // variable stays meaningful under a theme.
              var accentColors = {
                green: "var(--brand, #71f5a7)",
                blue: "var(--accent, #61a8ff)",
                violet: "var(--accent-2, #c5a3ff)",
              };
              var accent = Object.prototype.hasOwnProperty.call(accentColors, vars.accent)
                ? vars.accent
                : "green";
              var exitMode = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";

              root.style.setProperty("--bds-accent", accentColors[accent]);
              device.classList.add("bds-" + chromeKind);
              root.querySelector(".bds-pill").textContent = title;
              root.querySelector(".bds-bartitle").textContent = title;

              // SLOTS. Caller templates live at HOST DOCUMENT level (the mount
              // wipes host-clip children, and templates never render). The
              // scoped `document` proxy filters queries to this composition's
              // subtree, so the lookup deliberately goes through ownerDocument.
              var hostDoc = root.ownerDocument;
              function slotContent(name) {
                var tpl = null;
                try {
                  tpl = hostDoc.querySelector(
                    'template[data-slot="browser-device-stage-' + name + '"]',
                  );
                } catch (error) {
                  tpl = null;
                }
                return tpl ? hostDoc.importNode(tpl.content, true) : null;
              }

              var skeleton = screenA.querySelector(".bds-skel");
              var slotA = slotContent("screen");
              var screenB = null;
              if (swapAt > 0) {
                screenB = hostDoc.createElement("div");
                screenB.className = "bds-screen-b";
                var slotB = slotContent("screen-b");
                if (slotB) {
                  screenB.appendChild(slotB);
                } else {
                  var skeletonB = skeleton.cloneNode(true);
                  skeletonB.classList.add("bds-skel-b");
                  screenB.appendChild(skeletonB);
                }
                screen.appendChild(screenB);
              }
              if (slotA) {
                skeleton.remove();
                screenA.appendChild(slotA);
              }

              // Envelope: fixed IN/OUT, elastic HOLD, never time-scaled.
              var IN_BASE = 0.9;
              var OUT_BASE = exitMode === "none" ? 0 : 0.5;
              var SWAP_BASE = 0.55;
              var STILLNESS = 0.35;
              var duration = Math.max(0.001, parseFloat(root.dataset.duration || "5"));
              var totalBase = IN_BASE + OUT_BASE;
              var scale = duration < totalBase ? duration / totalBase : 1;
              var IN = IN_BASE * scale;
              var OUT = OUT_BASE * scale;
              var HOLD = Math.max(0, duration - (IN + OUT));
              var OUT_START = IN + HOLD;

              // Explicit both-endpoint state so a seek to t=0 is always correct.
              gsap.set(stage, { opacity: 0, y: "4.5cqh", scale: 0.965, transformOrigin: "50% 60%" });
              if (screenB) gsap.set(screenB, { opacity: 0, y: "1.6cqh" });

              var tl = gsap.timeline({ paused: true });

              // IN: one settle. Rise + soft scale + fade, smooth ease-out.
              tl.to(stage, { opacity: 1, duration: IN * 0.66, ease: "power2.out" }, 0);
              tl.to(stage, { y: "0cqh", duration: IN, ease: "power3.out" }, 0);
              tl.to(stage, { scale: 1, duration: IN, ease: "power2.out" }, 0);

              // HOLD: barely-there drift as two explicit tweens (no yoyo
              // re-tweens), then authored stillness until OUT.
              var driftHalf = Math.max(0, HOLD - STILLNESS) / 2;
              if (driftHalf >= 0.25) {
                driftHalf = Math.min(driftHalf, 1.6);
                tl.to(stage, { y: "-0.5cqh", duration: driftHalf, ease: "sine.inOut" }, IN);
                tl.to(stage, { y: "0cqh", duration: driftHalf, ease: "sine.inOut" }, IN + driftHalf);
              }

              // Optional screen swap: fade-through, clamped inside
              // [IN, D - OUT - SWAP] so it never fights the entrance or the
              // exit. Screen A empties completely onto the bare stage before
              // screen B rises in, so every frame shows at most one screen
              // (no double-exposure furniture on a paused frame).
              if (screenB) {
                var SWAP = SWAP_BASE * scale;
                var latest = Math.max(IN, duration - OUT - SWAP - 0.05);
                var swapStart = Math.min(Math.max(swapAt, IN), latest);
                var swapHalf = SWAP / 2;
                tl.to(screenA, { opacity: 0, duration: swapHalf, ease: "power1.in" }, swapStart);
                tl.fromTo(
                  screenB,
                  { opacity: 0, y: "1.6cqh" },
                  { opacity: 1, y: "0cqh", duration: swapHalf, ease: "power2.out" },
                  swapStart + swapHalf,
                );
              }

              // OUT: only when exit != none; the default holds the last frame.
              if (exitMode !== "none") {
                tl.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
                if (exitMode === "up") {
                  tl.to(stage, { y: "-4cqh", duration: OUT, ease: "power2.in" }, OUT_START);
                }
              }

              tl.seek(0);
              window.__timelines = window.__timelines || {};
              window.__timelines["browser-device-stage"] = tl;
            })();
          </script>
        </div>
      </template>
    </body>
  </html>
  ```
</Accordion>

Tagged `stage` `product-demo` `device` `browser` `phone` `slot` `chrome`.

## Related topics

* [Browse the complete Catalog](/catalog)
* [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
* [Build a richer composition](/go-further)


## Related topics

- [Device Frame Stage](/catalog/components/device-frame-stage.md)
- [VFX and liquid glass](/prompting/vfx-and-liquid-glass.md)
- [Multi Device Splay](/catalog/components/multi-device-splay.md)
