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

# Zoom Through Transition

> A transition that zooms through a focal card into the next scene.

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

## Install

<InstallCommand command="npx hyperframes add zoom-through-transition" />

That writes one file: `compositions/components/zoom-through-transition.html`.

## 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                                                                                                                                       |
| ------------ | ------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `from-label` | `Before`                  | string                     | Caption on the outgoing scene, the one on screen when the transition starts.                                                                       |
| `to-label`   | `Zoom Through Transition` | string                     | Caption on the incoming scene, the one the timeline wipes into frame.                                                                              |
| `depth`      | `standard`                | `close`, `standard`, `far` | Multiplies how far both scenes travel and how hard the outgoing one blurs, so the push covers a shorter or longer run in the same time.            |
| `accent`     | `blue`                    | `blue`, `green`, `violet`  | Theme token for the ring marking the incoming scene. Blue rides --accent, green --brand, violet --accent-2; each falls back to a neutral hairline. |

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="zoom-through-transition"
  data-composition-src="compositions/components/zoom-through-transition.html"
  data-variable-values='{"from-label":"Before","to-label":"Zoom Through Transition","depth":"standard","accent":"blue"}'
></div>
```

## Source

<Accordion title={`zoom-through-transition.html`}>
  ```html theme={null}
  <!doctype html>
  <!--
    Zoom Through Transition - a scene push that wipes the incoming scene through
    the outgoing one while the outgoing one drifts back and blurs.

    Mount contract: MOUNTABLE SUB-COMPOSITION. The runtime clones only <template>
    contents; #root fills the host box (inset: 0, container-type: size), and the
    file registers one paused timeline under the literal "zoom-through-transition"
    key. Variables come from window.__hyperframes.getVariables().

    This file owns its motion. It previously shipped as a fragment whose only
    executable timeline lived in demo.html, so an installed copy rendered the
    finished pose and never moved.

    Variables (declared in data-composition-variables below):
      - from-label (string, default "Before"): caption on the outgoing scene.
      - to-label (string, default "Zoom Through Transition"): caption on the
        incoming scene, clipped out of frame until the wipe brings it in.
      - depth (close | standard | far, default standard): multiplies how far both
        scenes travel and how hard the outgoing one blurs. Every input it
        multiplies is zero at rest, so it collapses to the shipped pose there. It
        deliberately leaves the two scale tracks alone: the timeline drives those
        from their own start values, which a multiplier would distort.
      - accent (blue | green | violet, default blue): ink of the ring that marks
        the incoming scene. Both scenes are monochrome placeholder material; the
        ring is the one accent-coloured element. blue rides --accent, green rides
        --brand, violet rides --accent-2, and each falls back to a neutral
        hairline so an unthemed composition renders in black and white.

    Envelope: the push starts at 0.35s (clamped to fit shorter durations), runs
    0.9s, then holds the incoming scene for whatever is left.
  -->
  <html
    lang="en"
    data-composition-id="zoom-through-transition"
    data-composition-duration="3"
    data-composition-variables='[
      { "id": "from-label", "type": "string", "role": "content", "label": "Outgoing label", "description": "Caption on the outgoing scene, the one on screen when the transition starts.", "default": "Before" },
      { "id": "to-label", "type": "string", "role": "content", "label": "Incoming label", "description": "Caption on the incoming scene, the one the timeline wipes into frame.", "default": "Zoom Through Transition" },
      { "id": "depth", "type": "enum", "role": "motion", "label": "Depth", "description": "Multiplies how far both scenes travel and how hard the outgoing one blurs, so the push covers a shorter or longer run in the same time.", "default": "standard", "options": [{ "value": "close", "label": "Close" }, { "value": "standard", "label": "Standard" }, { "value": "far", "label": "Far" }] },
      { "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Theme token for the ring marking the incoming scene. Blue rides --accent, green --brand, violet --accent-2; each falls back to a neutral hairline.", "default": "blue", "options": [{ "value": "blue", "label": "Blue" }, { "value": "green", "label": "Green" }, { "value": "violet", "label": "Violet" }] }
    ]'
  >
    <head>
      <meta charset="UTF-8" />
      <title>Zoom Through Transition</title>
    </head>
    <body>
      <template>
        <div id="root" data-composition-id="zoom-through-transition" data-duration="3" data-fps="30">
          <style>
            *,
            *::before,
            *::after {
              box-sizing: border-box;
            }

            /* Root fills the host-owned box; the scenes measure against it. */
            #root {
              position: absolute;
              inset: 0;
              container-type: size;
              isolation: isolate;
              overflow: hidden;
              background: #111827;
            }

            .hf-remocn-zoom-through-transition {
              --hf-accent: var(--accent, rgba(255, 255, 255, 0.28));
              --hf-l1: rgba(255, 255, 255, 0.72);
              --hf-l3: rgba(255, 255, 255, 0.18);
              --hf-l4: rgba(255, 255, 255, 0.08);
              --hf-hair: rgba(255, 255, 255, 0.14);
              --hf-ink: #111827;
              --hf-muted: #6b7280;
              --hf-surface: #ffffff;
              color: var(--hf-ink);
              font-family: Inter, system-ui, sans-serif;
            }
            .hf-remocn-zoom-through-transition {
              position: absolute;
              inset: 0;
              overflow: hidden;
              background: #111827;
            }
            .hf-remocn-zoom-through-transition .scene {
              position: absolute;
              inset: 0;
              display: grid;
              place-items: center;
              font-weight: 900;
              font-size: 13.5cqmin;
              letter-spacing: -0.05em;
              color: #fff;
            }
            .hf-remocn-zoom-through-transition .from {
              background:
                repeating-linear-gradient(
                  90deg,
                  rgba(255, 255, 255, 0.04) 0 1px,
                  transparent 1px 4px
                ),
                var(--hf-l4);
              transform: translate(
                  calc(var(--hf-from-x, 0px) * var(--hf-depth, 1)),
                  calc(var(--hf-from-y, 0px) * var(--hf-depth, 1))
                )
                scale(var(--hf-from-scale, 1));
              filter: blur(calc(var(--hf-from-blur, 0px) * var(--hf-depth, 1)));
            }
            .hf-remocn-zoom-through-transition .to {
              background:
                repeating-linear-gradient(
                  90deg,
                  rgba(255, 255, 255, 0.05) 0 1px,
                  transparent 1px 4px
                ),
                var(--hf-l3);
              box-shadow: inset 0 0 0 2px var(--hf-accent);
              transform: translate(
                  calc(var(--hf-to-x, 0px) * var(--hf-depth, 1)),
                  calc(var(--hf-to-y, 0px) * var(--hf-depth, 1))
                )
                scale(var(--hf-to-scale, 1));
              clip-path: inset(
                var(--hf-clip-top, 0%) var(--hf-clip-right, 100%) var(--hf-clip-bottom, 0%)
                  var(--hf-clip-left, 0%)
              );
            }
            .hf-remocn-zoom-through-transition .fx {
              position: absolute;
              inset: 0;
              background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.75), transparent);
              mix-blend-mode: screen;
              transform: translateX(var(--hf-fx-x, -110%));
            }
            .hf-remocn-zoom-through-transition .rgb {
              position: absolute;
              inset: 0;
              pointer-events: none;
              opacity: var(--hf-rgb-opacity, 0);
              background: linear-gradient(
                90deg,
                rgba(239, 68, 68, 0.35),
                transparent,
                rgba(34, 211, 238, 0.35)
              );
              transform: translateX(var(--hf-rgb-x, 0px));
            }
          </style>

          <div class="hf-remocn-zoom-through-transition">
            >
            <div class="scene from">Before</div>
            <div class="scene to">Zoom Through Transition</div>
            <div class="fx"></div>
            <div class="rgb"></div>
          </div>

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

              var vars =
                window.__hyperframes && window.__hyperframes.getVariables
                  ? window.__hyperframes.getVariables()
                  : {};

              // Unrecognised overrides return to their declared defaults.
              var depths = { close: 0.55, standard: 1, far: 1.7 };
              // Each choice routes to a different contract token so the variable stays
              // meaningful under a theme rather than collapsing into one themed accent.
              var HAIRLINE = "rgba(255, 255, 255, 0.28)";
              var accents = {
                blue: "var(--accent, " + HAIRLINE + ")",
                green: "var(--brand, " + HAIRLINE + ")",
                violet: "var(--accent-2, " + HAIRLINE + ")",
              };

              function pick(table, value, fallback) {
                return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback;
              }

              function label(value) {
                return typeof value === "string" && value !== "" ? value : null;
              }

              var depth = depths[pick(depths, vars.depth, "standard")];
              var accent = accents[pick(accents, vars.accent, "blue")];
              var fromLabel = label(vars["from-label"]);
              var toLabel = label(vars["to-label"]);

              var roots = document.querySelectorAll(".hf-remocn-zoom-through-transition");
              var mountRoot = document.getElementById("root");
              for (var i = 0; i < roots.length; i += 1) {
                var root = roots[i];
                root.style.setProperty("--hf-depth", String(depth));
                root.style.setProperty("--hf-accent", accent);

                var fromScene = root.querySelector(".from");
                var toScene = root.querySelector(".to");
                if (fromLabel !== null && fromScene) {
                  fromScene.textContent = fromLabel;
                }
                if (toLabel !== null && toScene) {
                  toScene.textContent = toLabel;
                }
              }

              // The motion used to live only in this file's demo page, so an installed
              // copy rendered the finished pose and never moved. The timeline belongs
              // with the markup it drives: build it here, paused, and register it under
              // this composition's own id so the runtime seeks it frame by frame.
              if (!mountRoot || !window.gsap) return;

              var SELF = ".hf-remocn-zoom-through-transition";
              var duration = Math.max(0.001, parseFloat(mountRoot.dataset.duration || "3"));
              // The push reads as a transition only with a beat of the outgoing scene
              // before it. Clamped so the wipe always lands inside the duration.
              var PUSH = 0.9;
              var START = Math.max(0, Math.min(0.35, duration - PUSH));

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

              tl.fromTo(
                SELF + " .to",
                { "--hf-clip-right": "100%", "--hf-to-x": "60px", "--hf-to-scale": 1.04 },
                {
                  "--hf-clip-right": "0%",
                  "--hf-to-x": "0px",
                  "--hf-to-scale": 1,
                  duration: PUSH,
                  ease: "power3.inOut",
                },
                START,
              );
              tl.fromTo(
                SELF + " .from",
                { "--hf-from-x": "0px", "--hf-from-scale": 1, "--hf-from-blur": "0px" },
                {
                  "--hf-from-x": "-70px",
                  "--hf-from-scale": 0.96,
                  "--hf-from-blur": "5px",
                  duration: PUSH,
                  ease: "power3.inOut",
                },
                START,
              );
              tl.fromTo(
                SELF + " .fx",
                { "--hf-fx-x": "-110%" },
                { "--hf-fx-x": "110%", duration: 0.58, ease: "power2.inOut" },
                START + 0.18,
              );
              // repeat/yoyo resolves from absolute time on every seek, so the strobe is
              // reproducible at any frame rather than accumulated across ticks.
              tl.fromTo(
                SELF + " .rgb",
                { "--hf-rgb-opacity": 0, "--hf-rgb-x": "-16px" },
                {
                  "--hf-rgb-opacity": 1,
                  "--hf-rgb-x": "16px",
                  repeat: 3,
                  yoyo: true,
                  duration: 0.07,
                  ease: "steps(1)",
                },
                START + 0.32,
              );

              // HOLD: the incoming scene rests for whatever is left of the duration.
              tl.set({}, {}, duration);

              tl.seek(0);

              window.__timelines = window.__timelines || {};
              window.__timelines["zoom-through-transition"] = tl;
            })();
          </script>
        </div>
      </template>
    </body>
  </html>
  ```
</Accordion>

## Usage

Open `compositions/components/zoom-through-transition.html` and paste its contents into your composition. See the comment header in the file for timeline integration guidance.

Tagged `transition-primitive` `remocn-port`.

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

- [Scale Transitions](/catalog/blocks/transitions-scale.md)
- [Transitions](/prompting/transitions.md)
- [Cinematic Zoom](/catalog/blocks/cinematic-zoom.md)
