> ## Documentation Index
> Fetch the complete documentation index at: https://hyperframes-fix-nested-composition-media-inpoint.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Motion Blur

> Velocity-driven motion blur — samples element position each frame and applies a one-sided SVG feGaussianBlur ghost trail proportional to speed

export const InstallCommand = ({command, item}) => {
  const [copied, setCopied] = React.useState(false);
  const [tuned, setTuned] = React.useState("");
  React.useEffect(() => {
    if (!item) return;
    const read = () => {
      try {
        const raw = new URLSearchParams(window.location.search).get(`vars-${item}`);
        if (!raw) return setTuned("");
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return setTuned("");
        if (Object.keys(parsed).length === 0) return setTuned("");
        setTuned(` --vars '${JSON.stringify(parsed)}'`);
      } catch {
        setTuned("");
      }
    };
    read();
    window.addEventListener("hf-vars-changed", read);
    window.addEventListener("popstate", read);
    return () => {
      window.removeEventListener("hf-vars-changed", read);
      window.removeEventListener("popstate", read);
    };
  }, [item]);
  const fullCommand = `${command}${tuned}`;
  const copy = async () => {
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(fullCommand);
      } else {
        const previous = document.activeElement;
        const scratch = document.createElement("textarea");
        scratch.value = fullCommand;
        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">
        {fullCommand}
      </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="motion-blur 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/motion-blur.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/motion-blur.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add motion-blur" item="motion-blur" />

That writes one file: `compositions/components/motion-blur.html`.

## Source

<Accordion title={`motion-blur.html`}>
  ```html theme={null}
  <!--
    Motion Blur — velocity-driven directional motion blur.

    Usage: paste this snippet into your composition, then call
    attachMotionBlur() with any element animated by your GSAP timeline.

    The snippet hooks into the timeline's onUpdate callback, tracks the
    GSAP x/y position of each target frame-by-frame, and applies:
      1. Ghost copies of the element at increasing backward offsets with
         decreasing opacity — inherently one-sided, no forward blur component.
      2. A small Gaussian blur at the current position so the element looks
         in-motion (blurry) rather than sharp on top of the trail.
      3. An optional scaleX/Y stretch in the direction of travel (off by
         default; enable via stretchMax > 0 if you want the effect).

    Both effects clear automatically when the element decelerates to rest.

    Requirements:
    - Elements must be animated via GSAP x/y (transform), not left/top.
    - Call attachMotionBlur() AFTER defining all tweens, before
      window.__timelines registration.
    - GSAP must be loaded before this snippet executes.

    API:
      attachMotionBlur(selector, timeline, options?)

    Options:
      blurScale     — directional blur per px/s of velocity (default 0.008)
      blurMax       — max blur radius on the motion axis in px (default 20)
      stretchScale  — scaleX/Y added per px/s (default 0.0002)
      stretchMax    — max stretch factor above 1.0 (default 0, disabled)
      axis          — "x" | "y" | "both" (default "both")
  -->

  <script>
    (function () {
      if (!window._hfMbUid) window._hfMbUid = 0;

      window.attachMotionBlur = function (selector, tl, opts) {
        opts = opts || {};
        var blurScale = opts.blurScale !== undefined ? opts.blurScale : 0.008;
        var blurMax = opts.blurMax !== undefined ? opts.blurMax : 20;
        var stretchScale = opts.stretchScale !== undefined ? opts.stretchScale : 0.0002;
        var stretchMax = opts.stretchMax !== undefined ? opts.stretchMax : 0;
        var axis = opts.axis || "both";

        var items = Array.isArray(selector) ? selector : [selector];
        var targets = items.reduce(function (acc, s) {
          if (typeof s === "string") {
            document.querySelectorAll(s).forEach(function (el) {
              acc.push(el);
            });
          } else if (s instanceof Element) {
            acc.push(s);
          }
          return acc;
        }, []);

        // One SVG filter per target. Ghost copies placed behind → inherently one-sided.
        // Small top Gaussian makes element look blurry at current position, not sharp.
        var ns = "http://www.w3.org/2000/svg";
        var GHOST_CFG = [
          { frac: 0.25, slope: 0.55 },
          { frac: 0.55, slope: 0.28 },
          { frac: 1.0, slope: 0.1 },
        ];

        var state = targets.map(function (el) {
          var uid = "hf-mb-" + window._hfMbUid++;
          var svg = document.createElementNS(ns, "svg");
          svg.setAttribute("style", "position:absolute;width:0;height:0;overflow:hidden;");

          var filter = document.createElementNS(ns, "filter");
          filter.id = uid;
          filter.setAttribute("x", "-110%");
          filter.setAttribute("y", "-25%");
          filter.setAttribute("width", "260%");
          filter.setAttribute("height", "150%");

          var ghosts = GHOST_CFG.map(function (cfg, gi) {
            var feOff = document.createElementNS(ns, "feOffset");
            feOff.setAttribute("in", "SourceGraphic");
            feOff.setAttribute("dx", "0");
            feOff.setAttribute("dy", "0");
            feOff.setAttribute("result", "go" + gi);

            var feGB = document.createElementNS(ns, "feGaussianBlur");
            feGB.setAttribute("in", "go" + gi);
            feGB.setAttribute("stdDeviation", "0 0");
            feGB.setAttribute("result", "gb" + gi);

            var feCT = document.createElementNS(ns, "feComponentTransfer");
            feCT.setAttribute("in", "gb" + gi);
            feCT.setAttribute("result", "gf" + gi);
            var feFuncA = document.createElementNS(ns, "feFuncA");
            feFuncA.setAttribute("type", "linear");
            feFuncA.setAttribute("slope", String(cfg.slope));
            feCT.appendChild(feFuncA);

            filter.appendChild(feOff);
            filter.appendChild(feGB);
            filter.appendChild(feCT);
            return { feOff: feOff, feGB: feGB, frac: cfg.frac };
          });

          var feTopBlur = document.createElementNS(ns, "feGaussianBlur");
          feTopBlur.setAttribute("in", "SourceGraphic");
          feTopBlur.setAttribute("stdDeviation", "0 0");
          feTopBlur.setAttribute("result", "top");
          filter.appendChild(feTopBlur);

          var feMerge = document.createElementNS(ns, "feMerge");
          for (var mi = GHOST_CFG.length - 1; mi >= 0; mi--) {
            var mn = document.createElementNS(ns, "feMergeNode");
            mn.setAttribute("in", "gf" + mi);
            feMerge.appendChild(mn);
          }
          var mnTop = document.createElementNS(ns, "feMergeNode");
          mnTop.setAttribute("in", "top");
          feMerge.appendChild(mnTop);
          filter.appendChild(feMerge);

          svg.appendChild(filter);
          document.body.appendChild(svg);

          return {
            el: el,
            ghosts: ghosts,
            feTopBlur: feTopBlur,
            filterId: uid,
            prevX: parseFloat(gsap.getProperty(el, "x")) || 0,
            prevY: parseFloat(gsap.getProperty(el, "y")) || 0,
            prevTime: tl.time(),
          };
        });

        // tl.eventCallback("onUpdate") is not available in the HyperFrames renderer —
        // the runtime proxies the timeline object. Tween onUpdate fires on every seek.
        var _proxy = { t: 0 };
        tl.to(
          _proxy,
          {
            t: 1,
            duration: Math.max(tl.duration(), 0.1),
            ease: "none",
            onUpdate: function () {
              var time = tl.time();

              state.forEach(function (s) {
                var x = parseFloat(gsap.getProperty(s.el, "x")) || 0;
                var y = parseFloat(gsap.getProperty(s.el, "y")) || 0;
                var dt = time - s.prevTime;

                if (dt > 0.0005) {
                  var vx = axis !== "y" ? (x - s.prevX) / dt : 0;
                  var vy = axis !== "x" ? (y - s.prevY) / dt : 0;

                  var bx = Math.min(Math.abs(vx) * blurScale, blurMax);
                  var by = Math.min(Math.abs(vy) * blurScale, blurMax);
                  var bxFinal = axis !== "y" ? bx : Math.max(by * 0.08, 0.4);
                  var byFinal = axis !== "x" ? by : Math.max(bx * 0.08, 0.4);
                  var active = bx > 0.3 || by > 0.3;

                  if (active) {
                    s.el.style.filter = "url(#" + s.filterId + ")";
                    s.ghosts.forEach(function (g) {
                      var dx = axis !== "y" ? (vx >= 0 ? -bxFinal * g.frac : bxFinal * g.frac) : 0;
                      var dy = axis !== "x" ? (vy >= 0 ? -byFinal * g.frac : byFinal * g.frac) : 0;
                      g.feOff.setAttribute("dx", dx.toFixed(2));
                      g.feOff.setAttribute("dy", dy.toFixed(2));
                      var gbx =
                        axis !== "y"
                          ? (bxFinal * g.frac * 0.5).toFixed(2)
                          : Math.max(byFinal * g.frac * 0.04, 0.4).toFixed(2);
                      var gby =
                        axis !== "x"
                          ? (byFinal * g.frac * 0.5).toFixed(2)
                          : Math.max(bxFinal * g.frac * 0.04, 0.4).toFixed(2);
                      g.feGB.setAttribute("stdDeviation", gbx + " " + gby);
                    });
                    s.feTopBlur.setAttribute(
                      "stdDeviation",
                      (bxFinal * 0.15).toFixed(2) + " " + (byFinal * 0.15).toFixed(2),
                    );

                    if (stretchMax > 0) {
                      var sx = 1 + Math.min(Math.abs(vx) * stretchScale, stretchMax);
                      var sy = 1 + Math.min(Math.abs(vy) * stretchScale, stretchMax);
                      var ox = axis !== "y" ? (vx >= 0 ? "100% 50%" : "0% 50%") : "50% 50%";
                      var oy = axis !== "x" ? (vy >= 0 ? "50% 100%" : "50% 0%") : "50% 50%";
                      gsap.set(s.el, {
                        scaleX: axis !== "y" ? sx : 1,
                        scaleY: axis !== "x" ? sy : 1,
                        transformOrigin: axis === "x" ? ox : axis === "y" ? oy : "50% 50%",
                      });
                    }
                  } else {
                    s.el.style.filter = "";
                    s.ghosts.forEach(function (g) {
                      g.feOff.setAttribute("dx", "0");
                      g.feOff.setAttribute("dy", "0");
                      g.feGB.setAttribute("stdDeviation", "0 0");
                    });
                    s.feTopBlur.setAttribute("stdDeviation", "0 0");
                    if (stretchMax > 0) {
                      gsap.set(s.el, { scaleX: 1, scaleY: 1, transformOrigin: "50% 50%" });
                    }
                  }

                  s.prevX = x;
                  s.prevY = y;
                  s.prevTime = time;
                } else if (dt < -0.0005) {
                  s.el.style.filter = "";
                  s.ghosts.forEach(function (g) {
                    g.feOff.setAttribute("dx", "0");
                    g.feOff.setAttribute("dy", "0");
                    g.feGB.setAttribute("stdDeviation", "0 0");
                  });
                  s.feTopBlur.setAttribute("stdDeviation", "0 0");
                  if (stretchMax > 0) {
                    gsap.set(s.el, { scaleX: 1, scaleY: 1, transformOrigin: "50% 50%" });
                  }
                  s.prevX = x;
                  s.prevY = y;
                  s.prevTime = time;
                }
                // dt ≈ 0: double-fire — skip.
              });
            },
          },
          0,
        );
      };
    })();
  </script>

  <!--
    Timeline integration example:

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

    tl.fromTo("#my-box", { x: -100 }, { x: 1700, duration: 1.2, ease: "power3.inOut" }, 0.5);

    // Extend to data-duration so seeks past the last tween reach the blur callback.
    tl.set(document.body, {}, DATA_DURATION);

    // Call AFTER tweens, BEFORE window.__timelines registration.
    // attachMotionBlur adds a tracking tween with onUpdate — must be called after
    // tl.set()/tl.to() have established the final timeline duration.
    attachMotionBlur("#my-box", tl, { axis: "x" });

    window.__timelines = window.__timelines || {};
    window.__timelines["my-composition"] = tl;
  -->
  ```
</Accordion>

## Usage

Paste the snippet into your composition, then call `attachMotionBlur()` after your GSAP tweens and before registering `window.__timelines`.

```html theme={null}
<!-- Extend the timeline to data-duration before calling attachMotionBlur -->
tl.set(document.body, {}, DATA_DURATION);

attachMotionBlur("#my-box", tl, {
  axis: "x",      // "x" | "y" | "both"
  blurMax: 40,    // max blur radius in px (default 20)
});

window.__timelines = window.__timelines || {};
window.__timelines["my-composition"] = tl;
```

## How it works

Each target element gets its own SVG filter. On every timeline seek, `attachMotionBlur` samples the element's GSAP `x`/`y` position, computes velocity, and drives three SVG filter primitives:

1. **Ghost copies** — three faded, blurred copies of the element placed behind it at increasing offsets proportional to speed. Inherently one-sided: no forward blur.
2. **Top blur** — a small symmetric Gaussian at the current position so the element looks in-motion rather than crisp on top of the trail.

Blur scales linearly with velocity up to `blurMax`. Both the ghost trail and top blur clear automatically when the element decelerates to rest.

## Options

| Option         | Default  | Description                                         |
| -------------- | -------- | --------------------------------------------------- |
| `axis`         | `"both"` | Motion axis — `"x"`, `"y"`, or `"both"`             |
| `blurScale`    | `0.008`  | Blur per px/s of velocity                           |
| `blurMax`      | `20`     | Max blur radius on the motion axis (px)             |
| `stretchScale` | `0.0002` | scaleX/Y added per px/s (requires `stretchMax > 0`) |
| `stretchMax`   | `0`      | Max stretch above 1.0 — disabled by default         |

Tagged `effect` `motion-blur` `velocity` `animation` `physics`.

## Related topics

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