> ## 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.

# Screen Flow Carousel

> Two to five app screens ride a horizontal rail: one primary at center, neighbors receded, advancing on cues with a velocity-matched throw and a smooth long-tail catch while a mono caption swaps with each advance. Screens are slots with token skeleton defaults.

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="screen-flow-carousel 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/screen-flow-carousel.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/screen-flow-carousel.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add screen-flow-carousel" />

That writes one file: `compositions/components/screen-flow-carousel.html`.

## Paste it into your composition

Open `compositions/components/screen-flow-carousel.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                                                                                           |
| ---------- | ------- | ------------------------- | ------------------------------------------------------------------------------------------------------ |
| `screens`  | `3`     | 2 to 5, step 1            | How many screens ride the rail.                                                                        |
| `captions` | \`\`    | string                    | Comma-separated caption per screen. Blank entries use the authored defaults.                           |
| `cues`     | \`\`    | string                    | Comma-separated advance times in seconds from mount start. Blank entries use the authored even rhythm. |
| `accent`   | `green` | `green`, `blue`, `violet` | Token the active ring, caption dot, and skeleton accents ride.                                         |
| `exit`     | `none`  | `none`, `fade`, `up`      | Optional departure. Default none: the final screen holds until the cut.                                |

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="screen-flow-carousel"
  data-composition-src="compositions/components/screen-flow-carousel.html"
  data-variable-values='{"screens":3,"captions":"","cues":"","accent":"green","exit":"none"}'
></div>
```

## Source

<Accordion title={`screen-flow-carousel.html`}>
  ```html theme={null}
  <!doctype html>
  <!--
    screen-flow-carousel -- HyperFrames video primitive (product demo / feature tour)

    Two to five app screens ride a horizontal rail. One screen is primary at
    center while its neighbors recede (smaller and dimmer). On each cue the
    whole rail advances one screen with a velocity-matched throw: a fast
    lateral translate that loses speed quickly, then a smooth long-tail catch
    onto the next center. This is the velocity-throw-snap motion law with the
    snap softened to the smooth register: no overshoot, no back ease, both
    sides of the rail always moving the same direction. A mono caption under
    the rail swaps with each advance.

    Screens are SLOTS. 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 slots live at document level):

      <template data-slot="screen-flow-carousel-screen-1"> ... </template>
      <template data-slot="screen-flow-carousel-screen-2"> ... </template>
      ... up to screen-5.

    Slot content may be an <img>, a muted <video>, or arbitrary HTML; direct
    img/video children are stretched to cover the screen. Any screen without a
    slot renders a token-styled skeleton; the four skeleton variants (list,
    cards, chart, detail) cycle by index so adjacent defaults never repeat.

    Variables (declared in data-composition-variables below):
      - screens (number 2 to 5, default 3): how many screens ride the rail.
      - captions (string, default ""): comma-separated caption per screen.
        Blank entries fall back to the authored defaults.
      - cues (string, default ""): comma-separated advance times in seconds
        relative to mount start (screens - 1 advances). Blank or invalid
        entries use the authored even rhythm. Values are clamped inside the
        hold and kept far enough apart that throws never overlap.
      - 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 screen to the last pixel.

    Envelope, fixed IN and OUT with elastic HOLD only (never timeScale):
      IN_BASE  = 0.90s  one stage settle (rise + fade), screen 1 primary
      HOLD     = max(0, D - IN - OUT); all advances live here, clamped so the
                 last throw lands with stillness left before the cut
      OUT_BASE = 0.45s  only when exit is fade or up
      If D < IN_BASE + OUT_BASE, IN and OUT compress together. The advance
      rhythm re-spaces itself from whatever hold remains.

    Mount contract: the runtime clones only this template. #root fills the
    host box (no data-width/data-height, container-type: size, cq units), is
    styled via #root only, and registers one paused timeline under the LITERAL
    "screen-flow-carousel" key. Rail position is CSS-owned through the numeric
    --sfc-pos custom property; GSAP tweens only that number plus
    per-screen scale/opacity with explicit endpoints, so any seek order lands
    identical frames.
  -->
  <html
    lang="en"
    data-composition-id="screen-flow-carousel"
    data-composition-duration="5"
    data-composition-variables='[
      { "id": "screens", "type": "number", "role": "content", "label": "Screens", "description": "How many screens ride the rail.", "default": 3, "min": 2, "max": 5, "step": 1 },
      { "id": "captions", "type": "string", "role": "content", "label": "Captions", "description": "Comma-separated caption per screen. Blank entries use the authored defaults.", "default": "" },
      { "id": "cues", "type": "string", "role": "timing", "label": "Cues", "description": "Comma-separated advance times in seconds from mount start. Blank entries use the authored even rhythm.", "default": "" },
      { "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Token the active ring, caption dot, and skeleton accents ride.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
      { "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "Optional departure. Default none: the final screen holds until the cut.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
    ]'
  >
    <head>
      <meta charset="UTF-8" />
      <title>Screen Flow Carousel</title>
    </head>
    <body>
      <template>
        <div id="root" data-composition-id="screen-flow-carousel" data-duration="5" data-fps="30">
          <style>
            *,
            *::before,
            *::after {
              box-sizing: border-box;
            }

            #root {
              --sfc-w: 44;
              --sfc-gap: 4;
              --sfc-pos: 0;
              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;
            }

            .sfc-clip {
              position: absolute;
              inset: 0;
              overflow: hidden;
              background: var(--bg, transparent);
            }

            .sfc-stage {
              position: absolute;
              inset: 0;
              will-change: transform, opacity;
            }

            /* The rail: one flex row, position owned by the numeric custom
               property --sfc-pos (fractional index of the centered screen).
               GSAP tweens that number; CSS does all the geometry in cqw. */
            .sfc-rail {
              position: absolute;
              top: 9cqh;
              left: 0;
              display: flex;
              align-items: stretch;
              gap: calc(var(--sfc-gap) * 1cqw);
              width: max-content;
              height: 64cqh;
              transform: translateX(
                calc(
                  (50 - var(--sfc-w) / 2 - var(--sfc-pos) * (var(--sfc-w) + var(--sfc-gap))) * 1cqw
                )
              );
              will-change: transform;
            }

            .sfc-screen {
              position: relative;
              flex: 0 0 calc(var(--sfc-w) * 1cqw);
              overflow: hidden;
              border: 0.16cqmin solid color-mix(in srgb, var(--border, #475569) 70%, transparent);
              border-radius: var(--radius, 2.4cqmin);
              background: color-mix(in srgb, var(--surface, #0f141b) 94%, var(--bg, #05070c));
              box-shadow: 0 2.6cqh 7cqh color-mix(in srgb, var(--bg, #000000) 42%, transparent);
              transform-origin: 50% 50%;
              will-change: transform, opacity;
            }

            .sfc-body {
              position: absolute;
              inset: 0;
              overflow: hidden;
            }

            .sfc-body > img,
            .sfc-body > video {
              display: block;
              width: 100%;
              height: 100%;
              object-fit: cover;
            }

            /* Active ring: opacity-only accent border so no var() color is
               ever GSAP-tweened. */
            .sfc-ring {
              position: absolute;
              inset: 0;
              z-index: 2;
              border: 0.24cqmin solid color-mix(in srgb, var(--sfc-accent) 78%, transparent);
              border-radius: inherit;
              opacity: 0;
              pointer-events: none;
            }

            /* Caption band: absolutely stacked mono captions, one visible. */
            .sfc-captions {
              position: absolute;
              top: 78cqh;
              left: 10cqw;
              right: 10cqw;
              height: 12cqh;
            }

            .sfc-caption {
              position: absolute;
              inset: 0;
              display: flex;
              align-items: center;
              justify-content: center;
              gap: var(--space-1, 1.4cqmin);
              overflow: hidden;
              color: var(--muted, #8b93a3);
              font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
              font-size: 2.7cqmin;
              font-weight: 500;
              letter-spacing: 0.12em;
              white-space: nowrap;
              will-change: transform, opacity;
            }

            .sfc-caption::before {
              content: "";
              flex: 0 0 auto;
              width: 1.1cqmin;
              height: 1.1cqmin;
              border-radius: 50%;
              background: var(--sfc-accent);
            }

            /* Skeleton furniture: pure token surfaces, no branding, weights
               stay in the register (blocks, no bold type). */
            .sfc-skel {
              position: absolute;
              inset: 0;
              display: flex;
              flex-direction: column;
              gap: var(--space-2, 2cqmin);
              padding: var(--space-3, 2.8cqmin);
            }

            .sfc-skel-heading {
              flex: 0 0 auto;
              width: 38%;
              height: 2.3cqmin;
              border-radius: 999px;
              background: color-mix(in srgb, var(--fg, #f8fafc) 22%, transparent);
            }

            .sfc-skel-line {
              flex: 0 0 auto;
              height: 1.2cqmin;
              border-radius: 999px;
              background: color-mix(in srgb, var(--fg, #f8fafc) 11%, transparent);
            }

            .sfc-skel-row {
              display: flex;
              flex: 0 0 auto;
              align-items: center;
              gap: var(--space-1, 1.6cqmin);
            }

            .sfc-skel-row .sfc-skel-line {
              flex: 1 1 auto;
            }

            .sfc-skel-dot {
              flex: 0 0 auto;
              width: 1.7cqmin;
              height: 1.7cqmin;
              border-radius: 50%;
              background: color-mix(in srgb, var(--fg, #f8fafc) 16%, transparent);
            }

            .sfc-skel-row:first-of-type .sfc-skel-dot {
              background: color-mix(in srgb, var(--sfc-accent) 78%, transparent);
            }

            .sfc-skel-grid {
              display: grid;
              flex: 1;
              grid-template-columns: repeat(2, minmax(0, 1fr));
              gap: var(--space-2, 2cqmin);
              min-height: 0;
            }

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

            .sfc-skel-card .sfc-skel-line {
              width: 60%;
            }

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

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

            .sfc-skel-bars {
              display: flex;
              flex: 1;
              align-items: flex-end;
              gap: var(--space-2, 2cqmin);
              min-height: 0;
              padding-top: 1cqmin;
            }

            .sfc-skel-bar {
              flex: 1;
              border-radius: 0.8cqmin 0.8cqmin 0 0;
              background: color-mix(in srgb, var(--fg, #f8fafc) 13%, transparent);
            }

            .sfc-skel-bar.sfc-skel-accent {
              background: color-mix(in srgb, var(--sfc-accent) 62%, transparent);
            }

            .sfc-skel-chip {
              flex: 0 0 auto;
              width: 11cqmin;
              height: 2.6cqmin;
              border: 0.14cqmin solid color-mix(in srgb, var(--sfc-accent) 55%, transparent);
              border-radius: 999px;
              background: color-mix(in srgb, var(--sfc-accent) 24%, transparent);
            }
          </style>

          <div
            id="screen-flow-carousel-clip"
            class="sfc-clip clip"
            data-start="0"
            data-duration="5"
            data-track-index="0"
          >
            <div class="sfc-stage">
              <div class="sfc-rail" role="list" aria-label="App screens"></div>
              <div class="sfc-captions" aria-hidden="true"></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(".sfc-stage");
              var rail = root.querySelector(".sfc-rail");
              var captionsBand = root.querySelector(".sfc-captions");
              var vars =
                window.__hyperframes && window.__hyperframes.getVariables
                  ? window.__hyperframes.getVariables()
                  : {};

              var screensValue = Number(vars.screens);
              var count = Number.isFinite(screensValue)
                ? Math.max(2, Math.min(5, Math.round(screensValue)))
                : 3;
              // 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("--sfc-accent", accentColors[accent]);

              var captionDefaults = ["Overview", "Details", "Insights", "Activity", "Settings"];
              var captionList = String(vars.captions == null ? "" : vars.captions).split(",");

              // 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="screen-flow-carousel-' + name + '"]',
                  );
                } catch (error) {
                  tpl = null;
                }
                return tpl ? hostDoc.importNode(tpl.content, true) : null;
              }

              // Skeleton defaults cycle four variants so adjacent screens read
              // as different app states. Pure token furniture, no branding.
              var lines = function (widths) {
                return widths
                  .map(function (width) {
                    return '<span class="sfc-skel-line" style="width:' + width + '"></span>';
                  })
                  .join("");
              };
              var listRows = ["86%", "72%", "80%", "64%", "76%", "58%"]
                .map(function (width) {
                  return (
                    '<span class="sfc-skel-row"><span class="sfc-skel-dot"></span>' +
                    '<span class="sfc-skel-line" style="max-width:' +
                    width +
                    '"></span></span>'
                  );
                })
                .join("");
              var card =
                '<span class="sfc-skel-card"><span class="sfc-skel-line"></span><span class="sfc-skel-block"></span></span>';
              var bars = ["34%", "58%", "42%", "76%", "50%", "64%"]
                .map(function (height, index) {
                  return (
                    '<span class="sfc-skel-bar' +
                    (index === 3 ? " sfc-skel-accent" : "") +
                    '" style="height:' +
                    height +
                    '"></span>'
                  );
                })
                .join("");
              var skeletons = [
                '<div class="sfc-skel"><span class="sfc-skel-heading"></span>' +
                  listRows +
                  '<span class="sfc-skel-block"></span></div>',
                '<div class="sfc-skel"><span class="sfc-skel-heading"></span><span class="sfc-skel-grid">' +
                  card +
                  card +
                  card +
                  card +
                  "</span></div>",
                '<div class="sfc-skel"><span class="sfc-skel-heading"></span><span class="sfc-skel-bars">' +
                  bars +
                  "</span>" +
                  lines(["46%"]) +
                  "</div>",
                '<div class="sfc-skel"><span class="sfc-skel-chip"></span><span class="sfc-skel-heading" style="width:56%"></span>' +
                  lines(["84%", "72%", "64%"]) +
                  '<span class="sfc-skel-block"></span></div>',
              ];

              var screens = [];
              var rings = [];
              var captions = [];
              for (var i = 0; i < count; i += 1) {
                var screen = hostDoc.createElement("article");
                screen.className = "sfc-screen";
                screen.setAttribute("role", "listitem");
                var body = hostDoc.createElement("div");
                body.className = "sfc-body";
                var slot = slotContent("screen-" + (i + 1));
                if (slot) {
                  body.appendChild(slot);
                } else {
                  body.innerHTML = skeletons[i % skeletons.length];
                }
                var ring = hostDoc.createElement("span");
                ring.className = "sfc-ring";
                screen.appendChild(body);
                screen.appendChild(ring);
                rail.appendChild(screen);
                screens.push(screen);
                rings.push(ring);

                var caption = hostDoc.createElement("span");
                caption.className = "sfc-caption";
                var captionText = (captionList[i] || "").trim();
                caption.textContent = captionText !== "" ? captionText : captionDefaults[i];
                captionsBand.appendChild(caption);
                captions.push(caption);
              }
              screens[0].setAttribute("aria-current", "true");

              // Envelope: fixed IN/OUT, elastic HOLD, never time-scaled. All
              // advances live inside the hold; spacing re-derives from whatever
              // hold the host duration leaves so throws can never overlap.
              var IN_BASE = 0.9;
              var OUT_BASE = exitMode === "none" ? 0 : 0.45;
              var ADV_BASE = 0.84;
              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 OUT_START = duration - OUT;
              var advanceCount = count - 1;
              var TAIL = Math.min(0.4, Math.max(0, duration - IN - OUT) * 0.12);
              var span = Math.max(0.05, duration - IN - OUT - TAIL);
              var spacing = span / (advanceCount + 1);
              var ADV = Math.min(ADV_BASE, spacing);
              var latest = Math.max(IN, duration - OUT - ADV - 0.05);

              var cueList = String(vars.cues == null ? "" : vars.cues).split(",");
              var starts = [];
              for (var k = 0; k < advanceCount; k += 1) {
                var raw = (cueList[k] || "").trim();
                var cue = Number(raw);
                var wanted = raw !== "" && Number.isFinite(cue) ? cue : IN + (k + 1) * spacing;
                var lower = k === 0 ? IN : starts[k - 1] + ADV;
                var upper = Math.max(lower, latest - (advanceCount - 1 - k) * ADV);
                starts.push(Math.min(Math.max(wanted, lower), upper));
              }

              // Explicit both-endpoint state so a seek to t=0 is always correct.
              gsap.set(stage, { opacity: 0, y: "4cqh" });
              gsap.set(rail, { "--sfc-pos": 0 });
              screens.forEach(function (element, index) {
                gsap.set(
                  element,
                  index === 0 ? { scale: 1, opacity: 1 } : { scale: 0.94, opacity: 0.62 },
                );
              });
              rings.forEach(function (element, index) {
                gsap.set(element, { opacity: index === 0 ? 1 : 0 });
              });
              captions.forEach(function (element, index) {
                gsap.set(
                  element,
                  index === 0 ? { opacity: 1, y: "0cqh" } : { opacity: 0, y: "1.2cqh" },
                );
              });

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

              // IN: one stage settle, screen 1 already primary.
              tl.to(stage, { opacity: 1, duration: IN * 0.7, ease: "power2.out" }, 0);
              tl.to(stage, { y: "0cqh", duration: IN, ease: "power3.out" }, 0);

              // Advances: the softened throw law. Fast travel sheds most of its
              // velocity under the whip-out ease, then a smooth long-tail catch
              // lands the exact next center. No overshoot, no back ease.
              var THROW_PART = 0.42;
              starts.forEach(function (at, index) {
                var target = index + 1;
                var THROW = ADV * THROW_PART;
                var CATCH = ADV - THROW;
                tl.to(rail, { "--sfc-pos": target - 0.14, duration: THROW, ease: "power4.out" }, at);
                tl.to(rail, { "--sfc-pos": target, duration: CATCH, ease: "power2.out" }, at + THROW);

                tl.to(
                  screens[index],
                  { scale: 0.94, opacity: 0.62, duration: ADV, ease: "power2.out" },
                  at,
                );
                tl.to(
                  screens[target],
                  { scale: 1, opacity: 1, duration: ADV, ease: "power2.out" },
                  at,
                );
                tl.to(rings[index], { opacity: 0, duration: ADV * 0.5, ease: "power1.out" }, at);
                tl.to(
                  rings[target],
                  { opacity: 1, duration: ADV * 0.6, ease: "power2.out" },
                  at + ADV * 0.4,
                );

                tl.to(
                  captions[index],
                  { opacity: 0, y: "-1.2cqh", duration: ADV * 0.4, ease: "power1.in" },
                  at,
                );
                tl.fromTo(
                  captions[target],
                  { opacity: 0, y: "1.2cqh" },
                  { opacity: 1, y: "0cqh", duration: ADV * 0.55, ease: "power2.out" },
                  at + ADV * 0.35,
                );
                tl.set(screens[index], { attr: { "aria-current": "false" } }, at);
                tl.set(screens[target], { attr: { "aria-current": "true" } }, at);
              });

              // 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["screen-flow-carousel"] = tl;
            })();
          </script>
        </div>
      </template>
    </body>
  </html>
  ```
</Accordion>

Tagged `product-demo` `feature-tour` `carousel` `rail` `slot` `screens`.

## 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

- [Checkout Flow](/catalog/components/checkout-flow.md)
- [Signup Flow](/catalog/components/signup-flow.md)
- [US Flow Map](/catalog/blocks/us-map-flow.md)
