diff --git a/docs/catalog/components/motion-blur.mdx b/docs/catalog/components/motion-blur.mdx
index 23c6c841474..d510e853130 100644
--- a/docs/catalog/components/motion-blur.mdx
+++ b/docs/catalog/components/motion-blur.mdx
@@ -1,6 +1,6 @@
---
title: "Motion Blur"
-description: "After Effects-style motion blur — integrates each element over a shutter window (shutter angle, phase, samples per frame) by sampling the GSAP timeline at sub-frame times and averaging equal-weight SVG copies along the real trajectory"
+description: "After Effects-style motion blur — integrates each element over a shutter window (shutter angle, phase, samples per frame) by sampling the GSAP timeline at sub-frame times and averaging equal-weight additive copies of the element along its real trajectory, so translation, scale, rotation, 3D rotation and skew all drive the smear"
---
import { InstallCommand } from "/snippets/install-command.jsx";
@@ -35,22 +35,31 @@ That writes one file: `compositions/components/motion-blur.html`.
1. After every timeline update (each rendered frame), the snippet seeks
the timeline to `samplesPerFrame` sub-frame times spread evenly across
- the shutter window, reads each target's GSAP x/y at every sample, then
- restores the frame time. Seeks run with events suppressed, so no user
- callbacks fire and the DOM ends up exactly where the frame left it.
- 2. An SVG filter stacks one copy of the element per sample (feOffset to
- the sampled position relative to the current one) and adds them at
- equal weight (feComposite arithmetic, 1/N each, sRGB space). Result:
- a box smear along the actual trajectory whose length is speed ×
- shutter time, with no decaying one-sided trail. The tails — where
- fewer copies overlap — step down evenly at 1/N per copy; wherever the
- element travels less than its own size the copies pile up past 1 and
- clamp, so the middle of the smear is solid rather than a plateau.
- 3. The element itself is then composited over that smear, sharp and at
- full opacity. No single duplicate ever exceeds 1/N, so a moving edge
- resolves as a staircase of ghosts behind a solid frame-time instance.
- 4. When the sampled positions collapse to the current one (element at
- rest), the filter is removed so the element renders sharp.
+ the shutter window, reads each target's RESOLVED `transform` at every
+ sample, then restores the frame time. Seeks run with events suppressed,
+ so no user callbacks fire and the DOM ends up exactly where the frame
+ left it. Reading the resolved matrix rather than a list of named GSAP
+ properties is what makes every driver work: translation, scale,
+ rotation, 3D rotation and skew arrive in the same one value, and a
+ driver nobody thought of needs no change here.
+ 2. One copy of the element per sample is stacked in an isolated group,
+ each carrying its sample's transform, each at 1/N opacity with
+ `mix-blend-mode: plus-lighter`. plus-lighter adds premultiplied colour,
+ so N+1 copies at 1/N sum to the average — the shutter integral. The
+ isolation is load-bearing: without a transparent backdrop of its own
+ the first copy would add onto the page and blow a light background out
+ to white. Result: a smear along the actual trajectory whose extent is
+ speed × shutter time, with no decaying one-sided trail. The tails —
+ where fewer copies overlap — step down evenly at 1/N per copy; wherever
+ the element travels less than its own size the copies pile up past 1
+ and clamp, so the middle of the smear is solid rather than a plateau.
+ 3. The element itself paints over that group, sharp and at full opacity.
+ No single copy ever exceeds 1/N, so a moving edge resolves as a
+ staircase of ghosts behind a solid frame-time instance. The group
+ carries the element's own opacity, so a beat that moves and fades at
+ once fades its smear with it.
+ 4. When every sampled transform collapses to the current one (element at
+ rest), the group is hidden so the element renders sharp.
Shutter window (AE semantics, per frame at time t, frame interval 1/fps):
shutterTime = shutterAngle / 360 / fps
@@ -71,9 +80,23 @@ That writes one file: `compositions/components/motion-blur.html`.
instance on top is what keeps total ink above the unblurred frame's.
Requirements:
- - Elements must be animated via GSAP x/y (transform), not left/top. The
- element's own scale/rotation is compensated; skew, 3D rotation and
- transformed ancestors are not.
+ - Elements must be animated via `transform` (GSAP x/y/scale/rotation and the
+ 3D and skew properties all qualify), not left/top. Transformed ancestors
+ are still not compensated.
+ - A 3D beat reads its perspective off the element's parent and applies it per
+ copy, so the vanishing point follows each copy's transform-origin rather
+ than the parent's perspective-origin. Those agree when the element is
+ centred in its perspective parent, which is how a 3D beat is authored.
+ - A copy drops the element's id but keeps its classes, because a class rule is
+ the only thing that can still style a copy's pseudo-elements. So a selector
+ written against a class matches the copies too: after attaching, address the
+ element by id or by reference, never by a class a copy also carries.
+ - A target is blurred once: a second attachMotionBlur() call naming the same
+ element leaves the first call's copies in place rather than stacking a
+ second set over them.
+ - Cost is N + 1 copies of the target's whole subtree, restyled on attach and
+ on resize and re-transformed every frame. Point it at the element that
+ moves, not at a container holding it.
- The host must render the timeline for the blur to update — HyperFrames
seeks every frame. A fresh paused timeline that is never seeked shows no
blur on its first frame.
@@ -96,12 +119,12 @@ That writes one file: `compositions/components/motion-blur.html`.
fps — composition frame rate (default: the root's data-fps,
else 30). Pass it explicitly when rendering with an
fps override (`hyperframes render --fps`).
- axis — "x" | "y" | "both" (default "both")
-->
+
+
+
+
+