@impractical-ai/motion 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,191 @@
1
1
  // Impractical Motion — public engine surface v1.1.
2
2
  //
3
3
  // A scene is two files:
4
- // App.tsx the stage markup (plain React + CSS; this is your product UI)
4
+ // App.tsx the stage markup (plain React + CSS) — the SET, not a
5
+ // replica of the product: what the shots need on screen
5
6
  // direction.ts the choreography — `export default (ctx: DirectionContext) => {...}`
6
7
  //
8
+ // COMPOSE THE SHOTS BEFORE YOU BUILD THE SET. Reconstruct the product first
9
+ // and you get a slideshow of panels. List the shots first — the set is what
10
+ // they need, and their components ARE the build list for App.tsx. Per shot:
11
+ // the CLAIM it proves, WHAT IS ON SCREEN, the FRAMING —
12
+ // in-situ a crop of the product, when where-it-lives is the point
13
+ // composed elements arranged for this shot alone, at a scale where the
14
+ // change reads as total; need not exist in the product. A
15
+ // picker beside a miniature of what it recolours beats either
16
+ // one filmed in place.
17
+ // full-frame a claim card; type owns the frame
18
+ // A dense product at 1920x1080 puts body text near 11px, so a wide shot of
19
+ // one establishes nothing. Frame the smallest thing that carries the meaning;
20
+ // widen only when it means something. If the film claims coherence, ONE shot
21
+ // shows the product whole.
22
+ //
23
+ // THE KIT LIBRARY. The types below are the ENGINE — the floor, not the
24
+ // craft. Most of the house craft is in the kit library, which is NOT in this
25
+ // file: call install_kits to write it into your draft, then import from
26
+ // './kit/<name>'. Every kit below is already written and tuned against
27
+ // reference footage; hand-rolling one is how a scene ends up looking it.
28
+ //
29
+ // HOW A FILM IS WRITTEN. The neutral film sequencer owns ordering; World
30
+ // Camera owns only named-object framing, depth, and camera gestures.
31
+ // const film = createFilmSequencer({ tl })
32
+ // const world = createWorldCamera({ tl, stage, camera, register: 'promo' })
33
+ // const copy = createTextPlans(text)
34
+ // const result = film.do(manner.realize('[data-obj="RESULT_ROWS"]'))
35
+ // film.do(world.camera.whipTo('RESULT_ROWS').landWith(result))
36
+ // film.do(copy.exit('[data-claim]', 'rise'))
37
+ // film.cut(editorial.cut(layers, next), world.camera.cutTo('NEXT'))
38
+ // Camera verbs: cutTo / moveTo / zoomTo / whipTo / departTo / angleTo /
39
+ // focusOn. The id is data-obj. Every verb centers that object's authored
40
+ // bounding box and maximizes it inside deterministic padding. Optional
41
+ // framing words are tight / normal / loose — breathing room, not zoom.
42
+ // film.do advances to settle, film.together shares a start, and
43
+ // film.at(handle.impact) attaches a consequence to its cause. film.cut
44
+ // waits for every outgoing plan END; coveredCut(handle, marker, reason, ...)
45
+ // is the explicit exception for an intentionally carried seam. Content
46
+ // manner and editorial cuts stay separate from World Camera. The raw camera
47
+ // in DirectionContext below
48
+ // (framing/full/crop/set/move) is the ESCAPE HATCH — measured or skeleton
49
+ // work that needs an exact pose at an exact time — not the default path.
50
+ //
51
+ // kit/kits — Side-effect import: the draft-side working-views engine (Studio
52
+ // reveal verbs on the currently deployed engine). Every kit imports this
53
+ // module,
54
+ // kit/reveal — DRAFT-SIDE working-views engine — GENERATED from
55
+ // src/stage/reveal.ts. Do not hand-edit.
56
+ // kit/actions — Actions — cursor, click, drag, scroll, and UI state
57
+ // primitives [exports: createActionsKit, createActionPlans]
58
+ // kit/ensemble — Ensemble choreography — words that move MANY elements as one
59
+ // system. [exports: createEnsembleKit]
60
+ // kit/looks — Looks — scene lighting, shading, ambient backdrops, and the
61
+ // hero shot [exports: createLooksKit]
62
+ // kit/sentence — The live-sentence kit: a sentence is a stateful object, not
63
+ // a static block. [exports: createSentenceKit]
64
+ // kit/morphkit — Universal shape morph — the shared-element architecture (how
65
+ // Framer Motion's layoutId / iOS matchedGeometryEffect / Flutter Hero do
66
+ // it): [exports: createMorphKit]
67
+ // kit/morphtree — GENERATED from src/stage/morph-tree.ts by
68
+ // build:morph-runtime. Do not edit. [exports: createMorphTreeKit]
69
+ // kit/highlight — Highlight — annotation primitives for pre-existing
70
+ // components [exports: createHighlightKit]
71
+ // kit/impact — Impact moves — the "slam": type or a card that hits the frame
72
+ // hard, or tips away out of it. [exports: createImpactKit]
73
+ // kit/streamkit — Streaming states — the choreographed WAITING that AI
74
+ // products live in. [exports: createStreamKit]
75
+ // kit/editorial — Editorial — cuts, jumps, and the camera-solved match cut
76
+ // [exports: createEditorialKit, createEditorialPlans]
77
+ // kit/depthmath — Depth math — pure, DOM-free, tested in isolation
78
+ // kit/depthkit — Depth — the stage as a physical world: layers, tilt, glass,
79
+ // light [exports: createDepthKit]
80
+ // kit/anchor — Anchor — never guess where another element is [exports:
81
+ // createAnchorKit]
82
+ // kit/atelier — Atelier — the house motion studies, callable from a film
83
+ // [exports: createAtelierKit]
84
+ // kit/cursor-geometry — Cursor geometry — project authored anchors into the
85
+ // cursor's actual plane.
86
+ // kit/film — Film — semantic sequencing without timestamp arithmetic
87
+ // [exports: createFilmSequencer]
88
+ // kit/manner — Manner kit — component-property motion (v2, the coverage
89
+ // bundle) [exports: createMannerKit, createMannerPlans]
90
+ // kit/morph-surface — Continuous vector surfaces with recursive content
91
+ // matching, scheduled by Film. [exports: createMorphSurfacePlans]
92
+ // kit/profile — Motion profile — loader, types, and the timing vocabulary
93
+ // kit/text — Text plans — full-span text timing for safe film boundaries
94
+ // [exports: createTextPlans]
95
+ // kit/world — World Camera — named subjects, spatial continuity, depth, and
96
+ // targeting [exports: createWorldCamera, createWorldKit]
97
+ //
98
+ // ─── MARKUP: what an element declares about itself ───────────────────────
99
+ //
100
+ // App.tsx is not inert scenery. The kits read ATTRIBUTES off the elements
101
+ // you write, so these are decisions made WHILE writing the markup: an
102
+ // element declares its identity, how it arrives, and how closely it can be
103
+ // looked at. The direction then only says WHEN. An attribute you never
104
+ // write is motion you never get, and it fails QUIET — manner.realize()
105
+ // over elements with no data-manner animates nothing.
106
+ //
107
+ // IDENTITY — what the camera, the profile, and the kits can name
108
+ // data-obj="rows" — this element IS an object and keeps that identity for
109
+ // the whole film. Camera poses name objects, never coordinates. Ids must
110
+ // be unique.
111
+ // data-part — a part of its object — what assemble() rises in sequence.
112
+ // Unmarked, its direct children are the parts.
113
+ // data-el — a member of a manner cascade — what 'cascade' births one at a
114
+ // time. Unmarked, the root's direct children are the members.
115
+ // data-product-ui — this element is the product shell. Legacy world.beat
116
+ // fits and camera angles use it as a coherent plane; camera-only target
117
+ // framing always measures the specifically named object.
118
+ // data-detail="fine" — legacy world.beat declaration that content survives
119
+ // a deep closeup. Camera-only target framing has no magnification cap or
120
+ // waiver. The only legal value is 'fine'.
121
+ //
122
+ // HOW IT ARRIVES — a property of the ELEMENT, not of the cue
123
+ // data-manner="<word>" — the element declares its own entrance; the
124
+ // direction only says WHEN, with manner.realize(sel, at). A
125
+ // space-separated list is legal and the direction names which fires.
126
+ // Without this attribute manner.realize() silently does nothing. The
127
+ // words:
128
+ // cascade — children write on, metronome births + settle
129
+ // accent-pop — accent pop onto an already-born element (never past rest
130
+ // size)
131
+ // pop — the item pop — the overshoot entrance
132
+ // slide-home — slides home whole on the measured entry deceleration…
133
+ // complete — appears COMPLETE in one frame — no fade
134
+ // breath — the surface takes one small breath
135
+ // grow — grows downward clipped to reveal its rows
136
+ // zoom-out — the SURFACE zooms itself out over a static world — a
137
+ // subject-carried device…
138
+ // snap — a hard ui-state change in ONE frame — never a fade. TWO FORMS,
139
+ // discriminated…
140
+ // spark — a small burst off the element's corner
141
+ // doodle — a rigid motif flies a measured arc (data-arc names it)
142
+ // reveal — an outcome row snaps on whole and settles up
143
+ // slide — peers slide one pitch to make room
144
+ // swap — covered-cut content arrives low and settles while the chrome holds
145
+ // still
146
+ // reorg — reorganized content slides up settling
147
+ // data-speed="quick" — 'snap' | 'quick' | 'slow'. On 'cascade' it picks the
148
+ // birth cadence class; elsewhere it scales the profile duration.
149
+ // Stepped-table manners take no speed (an error, not a no-op) and a bare
150
+ // number is always an error.
151
+ // data-anchor="50% 50%" — transform-origin for the scale-borne manners.
152
+ // data-late — on a cascade member: excluded from the cascade — the kit
153
+ // hides it and its own later cue reveals it.
154
+ // data-display-birth="flex" — members are display:none until born, then
155
+ // take this display — for members whose arrival displaces layout.
156
+ // data-visible-from="next-frame" — births render no earlier than one frame
157
+ // after the cascade starts: the mid-birth cut mask. The only value —
158
+ // times belong to the direction.
159
+ // data-snap-<property>="value" — for 'snap' on an element that STAYS on
160
+ // screen: the properties it lands on, set in one frame, e.g.
161
+ // data-snap-background="#5a5a5a". An element that IS the state visual
162
+ // snaps on whole and needs none.
163
+ // data-arc="<name>" — which measured arc a 'doodle' flies.
164
+ // data-from="left" — 'left' | 'right' | 'above' | 'below' — the side a
165
+ // 'slide' comes from (default 'left').
166
+ // data-focus — on a descendant: what a 'zoom-out' opens magnified on before
167
+ // pulling back.
168
+ //
169
+ // STAGE ROLES
170
+ // data-backdrop — you own what the backdrop IS — any markup, animated on
171
+ // the timeline; World Camera owns that it COVERS and automatically keeps
172
+ // one authored backdrop outside camera-space.
173
+ // data-frame-overlay — screen-fixed material above the camera world, such
174
+ // as continuous grain, vignette, scanlines, or letterbox bars. World
175
+ // Camera hoists it out of camera-space and keeps it viewport-sized.
176
+ // data-morph="name" — pairs a sub-component with the same name on the other
177
+ // side of a morph. Untagged children auto-pair by text.
178
+ // data-block — the blend-isolation host: filters and blend modes composite
179
+ // against this ancestor instead of leaking under the stage.
180
+ //
181
+ // ATELIER AND SCENE LIFECYCLE
182
+ // data-scene="id" — Register with world.scene(id); travel between
183
+ // registered scenes retires the scene being left. Re-enter it explicitly
184
+ // before revisiting.
185
+ // data-cadence — Optional manner/cadence grouping marker.
186
+ // data-atelier="id" — Wrap the skeleton returned by atelier_study; preserve
187
+ // its nested selector contract.
188
+ //
7
189
  // The timeline is created PAUSED and rendered by seeking frame by frame, so
8
190
  // every animation must be a function of timeline position alone. Never use
9
191
  // Date.now(), Math.random(), setTimeout, CSS animations, or autoplaying video —
@@ -25,24 +207,24 @@ export type TransitionDirection = 'left' | 'right' | 'up' | 'down'
25
207
 
26
208
  export type TransitionOpts = {
27
209
  color?: string
28
- /** Travel direction of the sweep (wipe/pixels). Default 'left'. */
210
+ /** Travel direction of the sweep (wipe/pixels). Default 'left'. */
29
211
  direction?: TransitionDirection
30
212
  duration?: number
31
213
  ease?: string
32
- /** Pixel cell size in px (pixels style). Default: frame height / 9. */
214
+ /** Pixel cell size in px (pixels style). Default: frame height / 9. */
33
215
  cell?: number
34
- /** Iris center as fractions of the frame, e.g. `{ x: 0.7, y: 0.4 }`. */
216
+ /** Iris center as fractions of the frame, e.g. `{ x: 0.7, y: 0.4 }`. */
35
217
  at?: { x?: number; y?: number }
36
218
  }
37
219
 
38
220
  export type TransitionResult = {
39
221
  duration: number
40
- /** The overlay's root element — e.g. to hide it manually mid-timeline. */
222
+ /** The overlay's root element — e.g. to hide it manually mid-timeline. */
41
223
  root: HTMLElement
42
224
  }
43
225
 
44
226
  /** What the camera dives through. Every mode computes its own geometry —
45
- * never hand-tune margins or wrap glyphs in spans to fake these. */
227
+ * never hand-tune margins or wrap glyphs in spans to fake these. */
46
228
  export type ThroughVia =
47
229
  | 'surface' // the element's face: its rect covers the frame (default)
48
230
  | 'glyph' // one character's COUNTER (the enclosed hole) covers the frame
@@ -50,99 +232,99 @@ export type ThroughVia =
50
232
  | 'anchor'
51
233
 
52
234
  export type ThroughOpts = {
53
- /** Dive mode. Default 'surface'. 'glyph' needs `char` (or `charIndex`),
54
- * 'word' needs `word`; both address plain text — no wrapper spans.
55
- * 'anchor' dives toward the element and, at the cut, match-frames the
56
- * element matching the SAME selector in the post-swap world (its twin),
57
- * so the subject persists while everything around it changes. */
235
+ /** Dive mode. Default 'surface'. 'glyph' needs `char` (or `charIndex`),
236
+ * 'word' needs `word`; both address plain text — no wrapper spans.
237
+ * 'anchor' dives toward the element and, at the cut, match-frames the
238
+ * element matching the SAME selector in the post-swap world (its twin),
239
+ * so the subject persists while everything around it changes. */
58
240
  via?: ThroughVia
59
- /** glyph mode: the character to dive through — first occurrence in the
60
- * target's text. The dive passes through its enclosed counter (o, e, a,
61
- * 0…); characters without a hole throw with a diagnostic. */
241
+ /** glyph mode: the character to dive through — first occurrence in the
242
+ * target's text. The dive passes through its enclosed counter (o, e, a,
243
+ * 0…); characters without a hole throw with a diagnostic. */
62
244
  char?: string
63
- /** glyph mode: 0-based index into the target's text, instead of `char`. */
245
+ /** glyph mode: 0-based index into the target's text, instead of `char`. */
64
246
  charIndex?: number
65
- /** word mode: the word to dive through — first occurrence. */
247
+ /** word mode: the word to dive through — first occurrence. */
66
248
  word?: string
67
- /** Takeover color. Defaults: surface/anchor — the target's effective
68
- * background (first non-transparent backgroundColor walking up from the
69
- * target); glyph/word — the surface BEHIND the text (the walk starts at
70
- * the text element's parent), i.e. what actually shows through the
71
- * counters. */
249
+ /** Takeover color. Defaults: surface/anchor — the target's effective
250
+ * background (first non-transparent backgroundColor walking up from the
251
+ * target); glyph/word — the surface BEHIND the text (the walk starts at
252
+ * the text element's parent), i.e. what actually shows through the
253
+ * counters. */
72
254
  color?: string
73
- /** Dive duration in seconds. Default 0.5. */
255
+ /** Dive duration in seconds. Default 0.5. */
74
256
  duration?: number
75
- /** Dive ease. Default 'power3.in' (accelerates into the surface). */
257
+ /** Dive ease. Default 'power3.in' (accelerates into the surface). */
76
258
  ease?: string
77
- /** Cover overshoot: 1.0 = the dive geometry (element rect, glyph counter,
78
- * or word rect per `via`) exactly covers the frame at the cut. Default
79
- * 1.15, so a few px of drift can never expose an edge. Override only for
80
- * taste — every mode measures its own geometry, so no mode NEEDS a
81
- * hand-tuned margin. */
259
+ /** Cover overshoot: 1.0 = the dive geometry (element rect, glyph counter,
260
+ * or word rect per `via`) exactly covers the frame at the cut. Default
261
+ * 1.15, so a few px of drift can never expose an edge. Override only for
262
+ * taste — every mode measures its own geometry, so no mode NEEDS a
263
+ * hand-tuned margin. */
82
264
  margin?: number
83
- /** The world swap, applied as seek-safe `tl.set`s at the exact cut moment
84
- * while the frame is fully covered: `hide` gets autoAlpha 0, `show` gets
85
- * autoAlpha 1. For anything richer, place your own `tl.set`s at the
86
- * returned `cutAt` time. */
265
+ /** The world swap, applied as seek-safe `tl.set`s at the exact cut moment
266
+ * while the frame is fully covered: `hide` gets autoAlpha 0, `show` gets
267
+ * autoAlpha 1. For anything richer, place your own `tl.set`s at the
268
+ * returned `cutAt` time. */
87
269
  swap?: { hide?: gsap.TweenTarget; show?: gsap.TweenTarget }
88
- /** Element in the post-swap world to ARRIVE from: at the cut the camera
89
- * snaps to this element's own cover framing (measured after the swap), so
90
- * releasing the overlay reveals it filling the frame, and the pull-out
91
- * starts inside the new world — the match-frame landing. Omit to pull out
92
- * from the dive's own framing. */
270
+ /** Element in the post-swap world to ARRIVE from: at the cut the camera
271
+ * snaps to this element's own cover framing (measured after the swap), so
272
+ * releasing the overlay reveals it filling the frame, and the pull-out
273
+ * starts inside the new world — the match-frame landing. Omit to pull out
274
+ * from the dive's own framing. */
93
275
  land?: string | Element
94
- /** Framing the pull-out settles on. Default the full frame
95
- * `{ scale: 1, x: 0, y: 0 }`. */
276
+ /** Framing the pull-out settles on. Default the full frame
277
+ * `{ scale: 1, x: 0, y: 0 }`. */
96
278
  settle?: { scale: number; x: number; y: number }
97
- /** Pull-out duration. Default 1.0. */
279
+ /** Pull-out duration. Default 1.0. */
98
280
  outDuration?: number
99
- /** Pull-out ease. Default 'expo.out'. */
281
+ /** Pull-out ease. Default 'expo.out'. */
100
282
  outEase?: string
101
283
  }
102
284
 
103
285
  export type ThroughResult = {
104
- /** Full span: dive + pull-out. */
286
+ /** Full span: dive + pull-out. */
105
287
  duration: number
106
- /** Absolute timeline time of the cut — the frame is guaranteed fully
107
- * covered here; schedule extra world-swap `tl.set`s at this time. */
288
+ /** Absolute timeline time of the cut — the frame is guaranteed fully
289
+ * covered here; schedule extra world-swap `tl.set`s at this time. */
108
290
  cutAt: number
109
- /** The takeover overlay's root element. */
291
+ /** The takeover overlay's root element. */
110
292
  root: HTMLElement
111
293
  }
112
294
 
113
295
  export type TransitionKit = {
114
- /** Cover the frame — put this at the end of a clip. Appends to the
115
- * timeline's end when no position is given. */
296
+ /** Cover the frame — put this at the end of a clip. Appends to the
297
+ * timeline's end when no position is given. */
116
298
  out: (
117
299
  style: TransitionStyle,
118
300
  opts?: TransitionOpts,
119
301
  position?: gsap.Position
120
302
  ) => TransitionResult
121
- /** Start covered and reveal the scene — put this at a clip's start
122
- * (position 0). Continues the same travel direction as its `out`. */
303
+ /** Start covered and reveal the scene — put this at a clip's start
304
+ * (position 0). Continues the same travel direction as its `out`. */
123
305
  in: (
124
306
  style: TransitionStyle,
125
307
  opts?: TransitionOpts,
126
308
  position?: gsap.Position
127
309
  ) => TransitionResult
128
- /** The go-through: dive the camera INTO something until its color takes
129
- * over the whole frame, cut worlds under the cover, then pull out into the
130
- * new world. NEVER hand-roll this (a magic-scale zoom + a hand-timed
131
- * `tl.set` swap + a re-aim) — through() computes the exact cover scale for
132
- * its dive geometry, measures it in its state AT THE DIVE'S START TIME
133
- * (entrances, moves, and tilts since build are all accounted for), and
134
- * snaps a full-frame overlay in the right color at the cut so the
135
- * takeover is guaranteed to the pixel. Four dive modes via `opts.via`:
136
- * 'surface' (default — the element's face), 'glyph' (through one
137
- * character's enclosed counter: `{ via: 'glyph', char: 'o' }` on plain
138
- * text, no wrapper span — the hole is measured from the actual glyph
139
- * outline, so the margin is computed, never tuned), 'word' (`{ via:
140
- * 'word', word: 'world' }` — the word's rect), and 'anchor' (match-cut:
141
- * dive toward the element, swap the world at the cut, and land on the
142
- * element matching the same selector in the new world, so the subject
143
- * persists while everything around it changes). Deterministic: every
144
- * value is resolved once at build; the swap is `tl.set`s, never
145
- * callbacks. Appends at the timeline's end when no position is given. */
310
+ /** The go-through: dive the camera INTO something until its color takes
311
+ * over the whole frame, cut worlds under the cover, then pull out into the
312
+ * new world. NEVER hand-roll this (a magic-scale zoom + a hand-timed
313
+ * `tl.set` swap + a re-aim) — through() computes the exact cover scale for
314
+ * its dive geometry, measures it in its state AT THE DIVE'S START TIME
315
+ * (entrances, moves, and tilts since build are all accounted for), and
316
+ * snaps a full-frame overlay in the right color at the cut so the
317
+ * takeover is guaranteed to the pixel. Four dive modes via `opts.via`:
318
+ * 'surface' (default — the element's face), 'glyph' (through one
319
+ * character's enclosed counter: `{ via: 'glyph', char: 'o' }` on plain
320
+ * text, no wrapper span — the hole is measured from the actual glyph
321
+ * outline, so the margin is computed, never tuned), 'word' (`{ via:
322
+ * 'word', word: 'world' }` — the word's rect), and 'anchor' (match-cut:
323
+ * dive toward the element, swap the world at the cut, and land on the
324
+ * element matching the same selector in the new world, so the subject
325
+ * persists while everything around it changes). Deterministic: every
326
+ * value is resolved once at build; the swap is `tl.set`s, never
327
+ * callbacks. Appends at the timeline's end when no position is given. */
146
328
  through: (
147
329
  target: string | Element,
148
330
  opts?: ThroughOpts,
@@ -223,39 +405,39 @@ export type CountOpts = {
223
405
  }
224
406
 
225
407
  export type TextKit = {
226
- /** Animate text in. Returns the total span so you can sequence after it. */
408
+ /** Animate text in. Returns the total span so you can sequence after it. */
227
409
  reveal: (
228
410
  target: string | Element,
229
411
  effect: RevealEffect,
230
412
  opts?: TextEffectOpts,
231
413
  position?: gsap.Position
232
414
  ) => TextResult
233
- /** Animate text out (mirror of reveal). */
415
+ /** Animate text out (mirror of reveal). */
234
416
  exit: (
235
417
  target: string | Element,
236
418
  effect: ExitEffect,
237
419
  opts?: TextEffectOpts,
238
420
  position?: gsap.Position
239
421
  ) => TextResult
240
- /** Deterministic number count-up on an element's text (for stat pills and
241
- * badges — pairs with a camera push-in). Snaps to integers on the
242
- * timeline; width is locked so the layout doesn't breathe. */
422
+ /** Deterministic number count-up on an element's text (for stat pills and
423
+ * badges — pairs with a camera push-in). Snaps to integers on the
424
+ * timeline; width is locked so the layout doesn't breathe. */
243
425
  count: (
244
426
  target: string | Element,
245
427
  opts: CountOpts,
246
428
  position?: gsap.Position
247
429
  ) => TextResult
248
- /** A settle-back gesture on a whole element (no split): pulse, wiggle,
249
- * shake. Great on CTAs and accent words after they appear. */
430
+ /** A settle-back gesture on a whole element (no split): pulse, wiggle,
431
+ * shake. Great on CTAs and accent words after they appear. */
250
432
  emphasize: (
251
433
  target: string | Element,
252
434
  effect: EmphasisEffect,
253
435
  opts?: EmphasisOpts,
254
436
  position?: gsap.Position
255
437
  ) => TextResult
256
- /** Draw a hand-sketched underline stroke beneath an element. The stroke
257
- * spans the element's box — target a tight element (an inline-block span
258
- * around the exact words), not a full-width heading. */
438
+ /** Draw a hand-sketched underline stroke beneath an element. The stroke
439
+ * spans the element's box — target a tight element (an inline-block span
440
+ * around the exact words), not a full-width heading. */
259
441
  underline: (
260
442
  target: string | Element,
261
443
  opts?: UnderlineOpts,
@@ -322,29 +504,29 @@ export type LogoResult = {
322
504
  }
323
505
 
324
506
  export type LogoKit = {
325
- /** Animate a logo in. Returns the total span so you can sequence after it. */
507
+ /** Animate a logo in. Returns the total span so you can sequence after it. */
326
508
  reveal: (
327
509
  target: string | Element,
328
510
  effect: LogoRevealEffect,
329
511
  opts?: LogoEffectOpts,
330
512
  position?: gsap.Position
331
513
  ) => LogoResult
332
- /** Animate a logo out (mirror of reveal). */
514
+ /** Animate a logo out (mirror of reveal). */
333
515
  exit: (
334
516
  target: string | Element,
335
517
  effect: LogoExitEffect,
336
518
  opts?: LogoEffectOpts,
337
519
  position?: gsap.Position
338
520
  ) => LogoResult
339
- /** A settle-back gesture on the whole mark: pulse, breathe, wiggle. */
521
+ /** A settle-back gesture on the whole mark: pulse, breathe, wiggle. */
340
522
  emphasize: (
341
523
  target: string | Element,
342
524
  effect: LogoEmphasisEffect,
343
525
  opts?: LogoEmphasisOpts,
344
526
  position?: gsap.Position
345
527
  ) => LogoResult
346
- /** A glint sweep across the mark. Follows the logo's silhouette (the
347
- * sheen is masked by the SVG shapes or the image alpha). */
528
+ /** A glint sweep across the mark. Follows the logo's silhouette (the
529
+ * sheen is masked by the SVG shapes or the image alpha). */
348
530
  shine: (
349
531
  target: string | Element,
350
532
  opts?: ShineOpts,
@@ -358,69 +540,69 @@ export type FilterTier = 'css' | 'svg'
358
540
 
359
541
  export type FilterEffect =
360
542
  // ── css tier ────────────────────────────────────────────────────────────
361
- /** Gaussian blur. Replaces the hand-rolled whip-pan "stage-blur spike". */
543
+ /** Gaussian blur. Replaces the hand-rolled whip-pan "stage-blur spike". */
362
544
  | 'blur'
363
- /** A halo in the element's own colour, from stacked drop-shadows. */
545
+ /** A halo in the element's own colour, from stacked drop-shadows. */
364
546
  | 'glow'
365
- /** Colour lifts (>1) or drains (<1). */
547
+ /** Colour lifts (>1) or drains (<1). */
366
548
  | 'saturate'
367
- /** Brightness pull-down — for backgrounding a layer under an overlay. */
549
+ /** Brightness pull-down — for backgrounding a layer under an overlay. */
368
550
  | 'dim'
369
- /** Hue rotation drift. */
551
+ /** Hue rotation drift. */
370
552
  | 'hue'
371
553
  // ── svg tier ────────────────────────────────────────────────────────────
372
- /** DIRECTIONAL blur along `angle`. CSS filter cannot do this at all. */
554
+ /** DIRECTIONAL blur along `angle`. CSS filter cannot do this at all. */
373
555
  | 'smear'
374
- /** Chromatic aberration: red and blue channels offset in opposite
375
- * directions. The glitch/impact look, without an asset. */
556
+ /** Chromatic aberration: red and blue channels offset in opposite
557
+ * directions. The glitch/impact look, without an asset. */
376
558
  | 'chroma'
377
- /** Turbulence-driven displacement: heat haze, liquid warp, dissolve. */
559
+ /** Turbulence-driven displacement: heat haze, liquid warp, dissolve. */
378
560
  | 'displace'
379
- /** Fractal-noise film grain composited over the source. */
561
+ /** Fractal-noise film grain composited over the source. */
380
562
  | 'grain'
381
- /** Bright-pass blur screened back over the source — bloom. */
563
+ /** Bright-pass blur screened back over the source — bloom. */
382
564
  | 'bloom'
383
- /** Posterize to N discrete levels per channel. */
565
+ /** Posterize to N discrete levels per channel. */
384
566
  | 'threshold'
385
567
 
386
568
  export type FilterOpts = {
387
- /** Peak value of the effect's primary parameter. */
569
+ /** Peak value of the effect's primary parameter. */
388
570
  amount?: number
389
- /** Secondary constant: turbulence frequency, bloom radius, grain scale. */
571
+ /** Secondary constant: turbulence frequency, bloom radius, grain scale. */
390
572
  detail?: number
391
573
  duration?: number
392
574
  ease?: string
393
- /** Direction in degrees for `smear` and `chroma`. 0 = horizontal. */
575
+ /** Direction in degrees for `smear` and `chroma`. 0 = horizontal. */
394
576
  angle?: number
395
- /** Tint for `glow` and `bloom`. Defaults to the element's computed colour. */
577
+ /** Tint for `glow` and `bloom`. Defaults to the element's computed colour. */
396
578
  color?: string
397
579
  }
398
580
 
399
581
  export type FilterResult = {
400
- /** Full span in seconds, including the return leg of a `spike`. */
582
+ /** Full span in seconds, including the return leg of a `spike`. */
401
583
  duration: number
402
584
  targets: Element[]
403
585
  }
404
586
 
405
587
  export type FilterKit = {
406
- /** Ramp an effect ON and leave it there — a state change. */
588
+ /** Ramp an effect ON and leave it there — a state change. */
407
589
  apply: (
408
590
  target: string | Element | Element[],
409
591
  effect: FilterEffect,
410
592
  opts?: FilterOpts,
411
593
  position?: gsap.Position
412
594
  ) => FilterResult
413
- /** Ramp it back to nothing. */
595
+ /** Ramp it back to nothing. */
414
596
  clear: (
415
597
  target: string | Element | Element[],
416
598
  effect: FilterEffect,
417
599
  opts?: FilterOpts,
418
600
  position?: gsap.Position
419
601
  ) => FilterResult
420
- /** In and out in ONE gesture: the whip-pan blur, the impact bloom, the
421
- * glitch chroma pop. One tween, so the Tweak panel shows ONE row with one
422
- * slider, one duration and one ease — instead of the two rows the
423
- * hand-rolled pattern produces, which the user has to keep in sync. */
602
+ /** In and out in ONE gesture: the whip-pan blur, the impact bloom, the
603
+ * glitch chroma pop. One tween, so the Tweak panel shows ONE row with one
604
+ * slider, one duration and one ease — instead of the two rows the
605
+ * hand-rolled pattern produces, which the user has to keep in sync. */
424
606
  spike: (
425
607
  target: string | Element | Element[],
426
608
  effect: FilterEffect,
@@ -432,21 +614,21 @@ export type FilterKit = {
432
614
  // ─── Masks — soft edges, mattes, fills, and blend modes ──────────────────
433
615
 
434
616
  export type MaskShape =
435
- /** Hard directional edge (clip-path inset) — same geometry as logo.wipe. */
617
+ /** Hard directional edge (clip-path inset) — same geometry as logo.wipe. */
436
618
  | 'wipe'
437
- /** SOFT directional edge. The capability clip-path structurally lacks. */
619
+ /** SOFT directional edge. The capability clip-path structurally lacks. */
438
620
  | 'feather'
439
- /** Hard circle growing from a point. */
621
+ /** Hard circle growing from a point. */
440
622
  | 'iris'
441
- /** Soft circle with a feathered falloff. */
623
+ /** Soft circle with a feathered falloff. */
442
624
  | 'radial'
443
- /** Letterbox opening from a horizontal centre line. */
625
+ /** Letterbox opening from a horizontal centre line. */
444
626
  | 'slit'
445
- /** Two panels parting from the centre outward. */
627
+ /** Two panels parting from the centre outward. */
446
628
  | 'barn'
447
- /** N bands opening together. */
629
+ /** N bands opening together. */
448
630
  | 'blinds'
449
- /** Caller-supplied clip-path or mask-image value — the escape hatch. */
631
+ /** Caller-supplied clip-path or mask-image value — the escape hatch. */
450
632
  | 'shape'
451
633
 
452
634
  export type BlendMode =
@@ -467,40 +649,40 @@ export type MatteKind = 'luma' | 'alpha'
467
649
 
468
650
  export type MaskOpts = {
469
651
  direction?: MaskDirection
470
- /** Feather width as a fraction of the element's box. 0 = hard edge. */
652
+ /** Feather width as a fraction of the element's box. 0 = hard edge. */
471
653
  softness?: number
472
- /** Band count for `blinds`. */
654
+ /** Band count for `blinds`. */
473
655
  bands?: number
474
- /** Centre for `iris` / `radial`, as fractions of the box. */
656
+ /** Centre for `iris` / `radial`, as fractions of the box. */
475
657
  at?: { x?: number; y?: number }
476
- /** For `shape`: any clip-path value. */
658
+ /** For `shape`: any clip-path value. */
477
659
  shape?: string
478
660
  duration?: number
479
661
  ease?: string
480
662
  }
481
663
 
482
664
  export type MatteOpts = {
483
- /** 'luma' uses the source's brightness, 'alpha' its transparency. */
665
+ /** 'luma' uses the source's brightness, 'alpha' its transparency. */
484
666
  kind?: MatteKind
485
- /** Sweep the matte across the target rather than holding it still. */
667
+ /** Sweep the matte across the target rather than holding it still. */
486
668
  sweep?: boolean
487
669
  duration?: number
488
670
  ease?: string
489
671
  }
490
672
 
491
673
  export type FillOpts = {
492
- /** A CSS gradient, e.g. 'linear-gradient(90deg,#ED1D24,#00B8FF)'. */
674
+ /** A CSS gradient, e.g. 'linear-gradient(90deg,#ED1D24,#00B8FF)'. */
493
675
  gradient?: string
494
- /** Or an image URL. */
676
+ /** Or an image URL. */
495
677
  image?: string
496
- /** How far the fill drifts across the glyphs, in percent. */
678
+ /** How far the fill drifts across the glyphs, in percent. */
497
679
  drift?: number
498
680
  duration?: number
499
681
  ease?: string
500
682
  }
501
683
 
502
684
  export type BlendOpts = {
503
- /** Opacity the layer fades up from. Default 0. */
685
+ /** Opacity the layer fades up from. Default 0. */
504
686
  from?: number
505
687
  duration?: number
506
688
  ease?: string
@@ -512,35 +694,35 @@ export type MaskResult = {
512
694
  }
513
695
 
514
696
  export type MaskKit = {
515
- /** Reveal ANY element through a moving mask — the general verb that
516
- * `text.reveal('wipe')` and `logo.reveal('iris')` are special cases of. */
697
+ /** Reveal ANY element through a moving mask — the general verb that
698
+ * `text.reveal('wipe')` and `logo.reveal('iris')` are special cases of. */
517
699
  reveal: (
518
700
  target: string | Element | Element[],
519
701
  shape: MaskShape,
520
702
  opts?: MaskOpts,
521
703
  position?: gsap.Position
522
704
  ) => MaskResult
523
- /** The mirror: mask the element back out. */
705
+ /** The mirror: mask the element back out. */
524
706
  hide: (
525
707
  target: string | Element | Element[],
526
708
  shape: MaskShape,
527
709
  opts?: MaskOpts,
528
710
  position?: gsap.Position
529
711
  ) => MaskResult
530
- /** Track matte: `source`'s silhouette becomes `target`'s visibility. */
712
+ /** Track matte: `source`'s silhouette becomes `target`'s visibility. */
531
713
  matte: (
532
714
  target: string | Element,
533
715
  source: string | Element,
534
716
  opts?: MatteOpts,
535
717
  position?: gsap.Position
536
718
  ) => MaskResult
537
- /** Fill an element's GLYPHS with a live layer and drift it. */
719
+ /** Fill an element's GLYPHS with a live layer and drift it. */
538
720
  fill: (
539
721
  target: string | Element,
540
722
  opts?: FillOpts,
541
723
  position?: gsap.Position
542
724
  ) => MaskResult
543
- /** Composite a layer against its beat with a blend mode. */
725
+ /** Composite a layer against its beat with a blend mode. */
544
726
  blend: (
545
727
  target: string | Element | Element[],
546
728
  mode: BlendMode,
@@ -554,20 +736,20 @@ export type MaskKit = {
554
736
  export type AxisName = 'wght' | 'wdth' | 'slnt' | 'opsz' | (string & {})
555
737
 
556
738
  export type AxisEffect =
557
- /** Ramp the axis from its current value to `to` and hold. */
739
+ /** Ramp the axis from its current value to `to` and hold. */
558
740
  | 'ramp'
559
- /** Ramp up and settle back — one breath. */
741
+ /** Ramp up and settle back — one breath. */
560
742
  | 'pulse'
561
- /** A per-character wave that travels through the line once. */
743
+ /** A per-character wave that travels through the line once. */
562
744
  | 'wave'
563
745
 
564
746
  export type AxisOpts = {
565
- /** Target axis value. Clamped to the face's declared range. */
747
+ /** Target axis value. Clamped to the face's declared range. */
566
748
  to?: number
567
749
  from?: number
568
750
  duration?: number
569
751
  ease?: string
570
- /** wave: seconds between adjacent characters. */
752
+ /** wave: seconds between adjacent characters. */
571
753
  stagger?: number
572
754
  }
573
755
 
@@ -586,15 +768,15 @@ export type AxisKit = {
586
768
  // ─── 3D — a Three.js layer under the deterministic timeline ──────────────
587
769
 
588
770
  export type ThreeLayerOpts = {
589
- /** Camera vertical FOV in degrees. */
771
+ /** Camera vertical FOV in degrees. */
590
772
  fov?: number
591
- /** Initial camera position. Default { x: 0, y: 0, z: 6 }. */
773
+ /** Initial camera position. Default { x: 0, y: 0, z: 6 }. */
592
774
  camera?: { x?: number; y?: number; z?: number }
593
- /** Layer opacity 0..1 (the canvas is a normal stage element). */
775
+ /** Layer opacity 0..1 (the canvas is a normal stage element). */
594
776
  opacity?: number
595
- /** Stacking: 'front' (default) composites the transparent layer above
596
- * the DOM; 'back' slots it behind — note an opaque App background will
597
- * hide a 'back' layer entirely. */
777
+ /** Stacking: 'front' (default) composites the transparent layer above
778
+ * the DOM; 'back' slots it behind — note an opaque App background will
779
+ * hide a 'back' layer entirely. */
598
780
  depth?: 'back' | 'front'
599
781
  }
600
782
 
@@ -613,29 +795,29 @@ export type ThreeCameraPose = {
613
795
  }
614
796
 
615
797
  export type ThreeFrameOpts = {
616
- /** Projected fraction of frame height occupied by the subject. Default 0.7. */
798
+ /** Projected fraction of frame height occupied by the subject. Default 0.7. */
617
799
  height?: number
618
- /** `contain` also enforces maxWidth; `height` may crop horizontally. */
800
+ /** `contain` also enforces maxWidth; `height` may crop horizontally. */
619
801
  mode?: 'contain' | 'height'
620
802
  maxWidth?: number
621
803
  angle?: ThreeCameraAngle
622
- /** Subject-to-camera direction. Overrides angle. */
804
+ /** Subject-to-camera direction. Overrides angle. */
623
805
  direction?: { x: number; y: number; z: number }
624
806
  roll?: number
625
807
  }
626
808
 
627
809
  export type ThreeMoveOpts = {
628
810
  duration?: number
629
- /** Camera travel ease. Defaults to the house dolly (`expo.inOut`), same as
630
- * the 2D camera; pass one only when the user asks for a different feel. */
811
+ /** Camera travel ease. Defaults to the house dolly (`expo.inOut`), same as
812
+ * the 2D camera; pass one only when the user asks for a different feel. */
631
813
  ease?: string
632
814
  }
633
815
 
634
816
  export type ThreeOrbitOpts = ThreeFrameOpts &
635
817
  ThreeMoveOpts & {
636
- /** Horizontal orbit delta in degrees. */
818
+ /** Horizontal orbit delta in degrees. */
637
819
  azimuth?: number
638
- /** Vertical orbit delta in degrees. */
820
+ /** Vertical orbit delta in degrees. */
639
821
  elevation?: number
640
822
  }
641
823
 
@@ -646,7 +828,7 @@ export type ThreeLightingPreset =
646
828
  | 'studio-neutral'
647
829
 
648
830
  export type ThreeLightingOpts = {
649
- /** Camera-relative stays flattering through a move; world-locked reveals form. */
831
+ /** Camera-relative stays flattering through a move; world-locked reveals form. */
650
832
  mode?: 'camera-relative' | 'world-locked'
651
833
  intensity?: number
652
834
  exposure?: number
@@ -662,30 +844,30 @@ export type ThreeLightRig = {
662
844
  }
663
845
 
664
846
  export type ThreeCameraRig = {
665
- /** Solve a pose whose projected subject bounds meet the framing constraint. */
847
+ /** Solve a pose whose projected subject bounds meet the framing constraint. */
666
848
  frame: (subject: THREE.Object3D, opts?: ThreeFrameOpts) => ThreeCameraPose
667
- /** Immediate deterministic camera cut to a resolved pose. */
849
+ /** Immediate deterministic camera cut to a resolved pose. */
668
850
  set: (pose: ThreeCameraPose) => void
669
- /** Tween XYZ, target, roll, and clipping planes while keeping the target locked. */
851
+ /** Tween XYZ, target, roll, and clipping planes while keeping the target locked. */
670
852
  moveTo: (
671
853
  pose: ThreeCameraPose,
672
854
  opts?: ThreeMoveOpts,
673
855
  position?: gsap.Position
674
856
  ) => { duration: number }
675
- /** Solve and dolly to a subject. */
857
+ /** Solve and dolly to a subject. */
676
858
  focus: (
677
859
  subject: THREE.Object3D,
678
860
  frame?: ThreeFrameOpts,
679
861
  move?: ThreeMoveOpts,
680
862
  position?: gsap.Position
681
863
  ) => { duration: number; pose: ThreeCameraPose }
682
- /** Orbit in camera-local spherical coordinates and re-solve framing. */
864
+ /** Orbit in camera-local spherical coordinates and re-solve framing. */
683
865
  orbit: (
684
866
  subject: THREE.Object3D,
685
867
  opts?: ThreeOrbitOpts,
686
868
  position?: gsap.Position
687
869
  ) => { duration: number; pose: ThreeCameraPose }
688
- /** Current planned pose, including moves not yet reached by the playhead. */
870
+ /** Current planned pose, including moves not yet reached by the playhead. */
689
871
  pose: () => ThreeCameraPose
690
872
  }
691
873
 
@@ -698,19 +880,19 @@ export type ThreeLightingRig = {
698
880
  }
699
881
 
700
882
  export type ThreeLayer = {
701
- /** The scene graph — add meshes, lights, groups. */
883
+ /** The scene graph — add meshes, lights, groups. */
702
884
  scene: THREE.Scene
703
885
  camera: THREE.PerspectiveCamera
704
- /** Subject-first XYZ camera control: exact projected framing, dolly, and orbit. */
886
+ /** Subject-first XYZ camera control: exact projected framing, dolly, and orbit. */
705
887
  rig: ThreeCameraRig
706
- /** Deterministic key/fill/rim presets bound to a 3D subject. */
888
+ /** Deterministic key/fill/rim presets bound to a 3D subject. */
707
889
  lighting: ThreeLightingRig
708
- /** The canvas element, for tweening opacity/filters like any element. */
890
+ /** The canvas element, for tweening opacity/filters like any element. */
709
891
  canvas: HTMLCanvasElement
710
- /** Drive the layer over [position, position+duration]. `update` runs every
711
- * frame with eased progress 0..1 BEFORE the render — set uniforms, camera
712
- * and material state from `t` alone (no clocks, no accumulation). Call
713
- * once per active span; overlapping drives render redundantly. */
892
+ /** Drive the layer over [position, position+duration]. `update` runs every
893
+ * frame with eased progress 0..1 BEFORE the render — set uniforms, camera
894
+ * and material state from `t` alone (no clocks, no accumulation). Call
895
+ * once per active span; overlapping drives render redundantly. */
714
896
  drive: (
715
897
  duration: number,
716
898
  update?: (t: number) => void,
@@ -720,17 +902,17 @@ export type ThreeLayer = {
720
902
  }
721
903
 
722
904
  export type ThreeKit = {
723
- /** Create a 3D layer sized to the stage. Everything async about it (first
724
- * compile, first paint) completes before window.__ready. */
905
+ /** Create a 3D layer sized to the stage. Everything async about it (first
906
+ * compile, first paint) completes before window.__ready. */
725
907
  layer: (opts?: ThreeLayerOpts) => ThreeLayer
726
908
  }
727
909
 
728
910
  // ─── Actions — stateful cursor actors, press, scroll, drag ───────────────
729
911
 
730
912
  /** Built-in cursor icons. `collab` is the Figma-style colored arrow + name
731
- * chip for multi-user scenes. Custom art passes `{ svg, hotspot }` — the
732
- * hotspot (0..1 of the icon box, where the tip sits) is part of the contract,
733
- * so brand cursors cannot reopen the misaligned-click bug. */
913
+ * chip for multi-user scenes. Custom art passes `{ svg, hotspot }` — the
914
+ * hotspot (0..1 of the icon box, where the tip sits) is part of the contract,
915
+ * so brand cursors cannot reopen the misaligned-click bug. */
734
916
  export type CursorIconName =
735
917
  | 'arrow'
736
918
  | 'pointer'
@@ -743,57 +925,57 @@ export type CursorIconName =
743
925
  export type CursorIcon = CursorIconName | { svg: string; hotspot: { x: number; y: number } }
744
926
 
745
927
  export type CursorActorOpts = {
746
- /** Icon set. Default 'arrow'; named collaborators usually want 'collab'. */
928
+ /** Icon set. Default 'arrow'; named collaborators usually want 'collab'. */
747
929
  icon?: CursorIcon
748
- /** Tint for the collab arrow and label chip. */
930
+ /** Tint for the collab arrow and label chip. */
749
931
  color?: string
750
- /** Figma-style name tag riding the cursor. */
932
+ /** Figma-style name tag riding the cursor. */
751
933
  label?: string
752
- /** Icon box size in scene px. Default 56 (at 1080p) — marketing videos
753
- * oversize the cursor so the action reads at a glance. */
934
+ /** Icon box size in scene px. Default 56 (at 1080p) — marketing videos
935
+ * oversize the cursor so the action reads at a glance. */
754
936
  size?: number
755
- /** 'still' (default) or 'human' — sub-pixel drift while parked between
756
- * actions, so a resting cursor never reads as a freeze-frame. */
937
+ /** 'still' (default) or 'human' — sub-pixel drift while parked between
938
+ * actions, so a resting cursor never reads as a freeze-frame. */
757
939
  idle?: 'still' | 'human'
758
- /** Auto-morph arrow → pointer over the last stretch of a click travel.
759
- * Default true. */
940
+ /** Auto-morph arrow → pointer over the last stretch of a click travel.
941
+ * Default true. */
760
942
  morph?: boolean
761
943
  }
762
944
 
763
945
  export type CursorMoveOpts = {
764
- /** Travel time in seconds. Default 0.8. */
946
+ /** Travel time in seconds. Default 0.8. */
765
947
  duration?: number
766
- /** Default 'ease-in-out-cubic' — every action eases in-out. */
948
+ /** Default 'ease-in-out-cubic' — every action eases in-out. */
767
949
  ease?: string
768
- /** Where on the target to land, as fractions of its box. Default
769
- * `{ x: 0.5, y: 0.52 }` — the corpus's observed natural click point. */
950
+ /** Target-box fractions, reprojected each frame after camera/target transforms.
951
+ * Default `{ x: 0.5, y: 0.52 }`. */
770
952
  anchor?: { x?: number; y?: number }
771
- /** Extra scene-px nudge after the anchor. */
953
+ /** Extra scene-px nudge after the anchor. */
772
954
  offset?: { x?: number; y?: number }
773
- /** First-action entrance: 'edge' (default — slides in from the nearest
774
- * frame edge) or 'fade' (materializes just short of the target, for tight
775
- * close-ups where the edge is far away). Ignored while already visible. */
955
+ /** First-action entrance: 'edge' (default — slides in from the nearest
956
+ * frame edge) or 'fade' (materializes just short of the target, for tight
957
+ * close-ups where the edge is far away). Ignored while already visible. */
776
958
  enter?: 'edge' | 'fade'
777
959
  }
778
960
 
779
961
  /** What the cursor does after the action — the continuity contract.
780
- * 'stay' (default) parks it where it is so the NEXT action continues from
781
- * there; 'exit' leaves via the nearest edge; 'park' steps aside just below
782
- * the target (clears it for typing or a reveal); a selector travels to that
783
- * element and parks there. */
962
+ * 'stay' (default) parks it where it is so the NEXT action continues from
963
+ * there; 'exit' leaves via the nearest edge; 'park' steps aside just below
964
+ * the target (clears it for typing or a reveal); a selector travels to that
965
+ * element and parks there. */
784
966
  export type CursorThen = 'stay' | 'exit' | 'park' | string
785
967
 
786
968
  export type CursorClickOpts = CursorMoveOpts & {
787
- /** Target squash scale. 'auto' (default) derives from the element's size —
788
- * big surfaces squash less. A number (e.g. 0.9) is used verbatim. */
969
+ /** Target squash scale. 'auto' (default) derives from the element's size —
970
+ * big surfaces squash less. A number (e.g. 0.9) is used verbatim. */
789
971
  press?: number | 'auto'
790
- /** Squash time each way. Default 0.09. */
972
+ /** Squash time each way. Default 0.09. */
791
973
  pressDuration?: number
792
974
  then?: CursorThen
793
975
  }
794
976
 
795
977
  export type CursorDragOpts = CursorMoveOpts & {
796
- /** Subtle scale lift on the payload while it is held. Default true. */
978
+ /** Subtle scale lift on the payload while it is held. Default true. */
797
979
  lift?: boolean
798
980
  then?: CursorThen
799
981
  }
@@ -801,50 +983,49 @@ export type CursorDragOpts = CursorMoveOpts & {
801
983
  export type CursorExitOpts = {
802
984
  duration?: number
803
985
  ease?: string
804
- /** 'edge' (default) or 'fade'. */
986
+ /** 'edge' (default) or 'fade'. */
805
987
  via?: 'edge' | 'fade'
806
988
  }
807
989
 
808
990
  export type CursorActor = {
809
- /** Move to an element (or continue there from the last position). */
991
+ /** Move to an element (or continue there from the last position). */
810
992
  to: (
811
993
  target: string | Element,
812
994
  opts?: CursorMoveOpts,
813
995
  position?: gsap.Position
814
996
  ) => { duration: number }
815
- /** Travel + press: squashes the cursor AND the target, ordered so the
816
- * click visibly precedes any result the direction schedules after it. */
997
+ /** Travel + press: squashes the cursor AND the target, ordered so the
998
+ * click visibly precedes any result the direction schedules after it. */
817
999
  click: (
818
1000
  target: string | Element,
819
1001
  opts?: CursorClickOpts,
820
1002
  position?: gsap.Position
821
1003
  ) => { duration: number }
822
- /** Grab a payload and carry it: cursor and payload move as ONE tween pair
823
- * with identical timing, so they cannot desync. `to` is an element/selector
824
- * (payload centers on it) or a scene-px delta `{ x, y }`. */
1004
+ /** Track the transformed payload each frame. `to`: same-plane element/selector
1005
+ * (centers the payload) or scene-px delta `{ x, y }`. */
825
1006
  drag: (
826
1007
  payload: string | Element,
827
1008
  to: string | Element | { x: number; y: number },
828
1009
  opts?: CursorDragOpts,
829
1010
  position?: gsap.Position
830
1011
  ) => { duration: number }
831
- /** Leave the frame. The next action re-enters fresh. */
1012
+ /** Leave the frame. The next action re-enters fresh. */
832
1013
  exit: (opts?: CursorExitOpts, position?: gsap.Position) => { duration: number }
833
- /** Update the collab name chip on the timeline. */
1014
+ /** Update the collab name chip on the timeline. */
834
1015
  label: (text: string, position?: gsap.Position) => void
835
1016
  }
836
1017
 
837
1018
  export type CursorKit = CursorActor & {
838
- /** Declare a named cursor — an independent actor with its own icon, color,
839
- * label chip, and continuity state. Two actors animate concurrently just by
840
- * overlapping their positions. */
1019
+ /** Declare a named cursor — an independent actor with its own icon, color,
1020
+ * label chip, and continuity state. Two actors animate concurrently just by
1021
+ * overlapping their positions. */
841
1022
  actor: (name: string, opts?: CursorActorOpts) => CursorActor
842
1023
  }
843
1024
 
844
1025
  export type PressOpts = {
845
- /** Squash scale; 'auto' (default) derives from element size. */
1026
+ /** Squash scale; 'auto' (default) derives from element size. */
846
1027
  amount?: number | 'auto'
847
- /** Each way. Default 0.09. */
1028
+ /** Each way. Default 0.09. */
848
1029
  duration?: number
849
1030
  ease?: string
850
1031
  }
@@ -852,46 +1033,46 @@ export type PressOpts = {
852
1033
  export type ScrollOpts = {
853
1034
  duration?: number
854
1035
  ease?: string
855
- /** Where the target settles in the container: 0 top, 0.5 center (default),
856
- * 1 bottom. */
1036
+ /** Where the target settles in the container: 0 top, 0.5 center (default),
1037
+ * 1 bottom. */
857
1038
  align?: number
858
1039
  }
859
1040
 
860
1041
  export type UiChange = {
861
- /** Replace the element's text content. */
1042
+ /** Replace the element's text content. */
862
1043
  text?: string
863
- /** Set an input's value. */
1044
+ /** Set an input's value. */
864
1045
  value?: string
865
- /** Swap an image/video source. */
1046
+ /** Swap an image/video source. */
866
1047
  src?: string
867
- /** Set attributes verbatim. */
1048
+ /** Set attributes verbatim. */
868
1049
  attrs?: Record<string, string>
869
- /** Add/remove classes (resolved against the authored class list). */
1050
+ /** Add/remove classes (resolved against the authored class list). */
870
1051
  classes?: { add?: string[]; remove?: string[] }
871
- /** Brief brightness pulse after the change, so the state change reads as an
872
- * event rather than a glitch. */
1052
+ /** Brief brightness pulse after the change, so the state change reads as an
1053
+ * event rather than a glitch. */
873
1054
  flash?: boolean
874
1055
  }
875
1056
 
876
1057
  export type ActionsKit = {
877
1058
  cursor: CursorKit
878
- /** Squash without a cursor — keyboard sends, programmatic actions. */
1059
+ /** Squash without a cursor — keyboard sends, programmatic actions. */
879
1060
  press: (
880
1061
  target: string | Element,
881
1062
  opts?: PressOpts,
882
1063
  position?: gsap.Position
883
1064
  ) => { duration: number }
884
- /** Animated scroll of a container until `to` (element, selector, or an
885
- * absolute scrollTop) is in view. `duration: 0` sets it instantly for
886
- * initial state. */
1065
+ /** Animated scroll of a container until `to` (element, selector, or an
1066
+ * absolute scrollTop) is in view. `duration: 0` sets it instantly for
1067
+ * initial state. */
887
1068
  scroll: (
888
1069
  container: string | Element,
889
1070
  to: string | Element | number,
890
1071
  opts?: ScrollOpts,
891
1072
  position?: gsap.Position
892
1073
  ) => { duration: number }
893
- /** Declarative hard UI state change on the timeline (seek-reversible).
894
- * Instant by design — a cut is hard; `flash` adds the one animated accent. */
1074
+ /** Declarative hard UI state change on the timeline (seek-reversible).
1075
+ * Instant by design — a cut is hard; `flash` adds the one animated accent. */
895
1076
  ui: {
896
1077
  set: (
897
1078
  target: string | Element,
@@ -906,37 +1087,37 @@ export type ActionsKit = {
906
1087
  export type HighlightRelease = 'fade' | 'undraw' | 'hold'
907
1088
 
908
1089
  export type HighlightOpts = {
909
- /** Stroke/fill color. Each primitive has its own sensible default. */
1090
+ /** Stroke/fill color. Each primitive has its own sensible default. */
910
1091
  color?: string
911
- /** Draw-on time in seconds. */
1092
+ /** Draw-on time in seconds. */
912
1093
  duration?: number
913
- /** Default 'ease-in-out-cubic'. */
1094
+ /** Default 'ease-in-out-cubic'. */
914
1095
  ease?: string
915
- /** Seconds the highlight holds after drawing, or 'hold' to stay to the end.
916
- * Default 'hold' — releasing is a deliberate choice. */
1096
+ /** Seconds the highlight holds after drawing, or 'hold' to stay to the end.
1097
+ * Default 'hold' — releasing is a deliberate choice. */
917
1098
  persist?: number | 'hold'
918
- /** How it leaves when persist is a number: 'fade' (default) or 'undraw'. */
1099
+ /** How it leaves when persist is a number: 'fade' (default) or 'undraw'. */
919
1100
  release?: Exclude<HighlightRelease, 'hold'>
920
- /** Extra space around the target in scene px. Default derives from size. */
1101
+ /** Extra space around the target in scene px. Default derives from size. */
921
1102
  padding?: number
922
- /** Stroke thickness in px. */
1103
+ /** Stroke thickness in px. */
923
1104
  thickness?: number
924
- /** What to fit: 'text' hugs the glyphs (a block element's box spans its
925
- * full column — a marker on "99.99%" must not swallow the empty half of
926
- * the card); 'box' takes the element box. Marker, circle, and underline
927
- * default to 'text'; ring, glow, and spotlight default to 'box'. */
1105
+ /** What to fit: 'text' hugs the glyphs (a block element's box spans its
1106
+ * full column — a marker on "99.99%" must not swallow the empty half of
1107
+ * the card); 'box' takes the element box. Marker, circle, and underline
1108
+ * default to 'text'; ring, glow, and spotlight default to 'box'. */
928
1109
  fit?: 'text' | 'box'
929
1110
  }
930
1111
 
931
1112
  export type SpotlightOpts = {
932
- /** How dark the rest of the frame goes (0..1). Default 0.55. */
1113
+ /** How dark the rest of the frame goes (0..1). Default 0.55. */
933
1114
  dim?: number
934
- /** Defocus everything OUTSIDE the cutout by this many px (the target stays
935
- * sharp). 0 (default) dims only. Engine ceiling: 20. */
1115
+ /** Defocus everything OUTSIDE the cutout by this many px (the target stays
1116
+ * sharp). 0 (default) dims only. Engine ceiling: 20. */
936
1117
  blur?: number
937
- /** Gaussian softness of the cutout edge in px. Default 26. */
1118
+ /** Gaussian softness of the cutout edge in px. Default 26. */
938
1119
  feather?: number
939
- /** Breathing room between the element's edge and the cutout, px. Default 16. */
1120
+ /** Breathing room between the element's edge and the cutout, px. Default 16. */
940
1121
  padding?: number
941
1122
  duration?: number
942
1123
  ease?: string
@@ -944,51 +1125,51 @@ export type SpotlightOpts = {
944
1125
  }
945
1126
 
946
1127
  export type HighlightKit = {
947
- /** Rounded ring draws itself around the element, following its corner
948
- * radius. The workhorse product-tour annotation. */
1128
+ /** Rounded ring draws itself around the element, following its corner
1129
+ * radius. The workhorse product-tour annotation. */
949
1130
  ring: (target: string | Element, opts?: HighlightOpts, position?: gsap.Position) => { duration: number }
950
- /** Hand-drawn ellipse loop with overshoot — red-pen annotation energy. */
1131
+ /** Hand-drawn ellipse loop with overshoot — red-pen annotation energy. */
951
1132
  circle: (target: string | Element, opts?: HighlightOpts, position?: gsap.Position) => { duration: number }
952
- /** Highlighter-pen swipe behind the glyphs: a skewed band wipes across in
953
- * multiply blend, sized from the line height. */
1133
+ /** Highlighter-pen swipe behind the glyphs: a skewed band wipes across in
1134
+ * multiply blend, sized from the line height. */
954
1135
  marker: (target: string | Element, opts?: HighlightOpts, position?: gsap.Position) => { duration: number }
955
- /** Underline draw. Styles: 'straight' | 'marker' | 'sketch'. */
1136
+ /** Underline draw. Styles: 'straight' | 'marker' | 'sketch'. */
956
1137
  underline: (
957
1138
  target: string | Element,
958
1139
  style?: 'straight' | 'marker' | 'sketch',
959
1140
  opts?: HighlightOpts,
960
1141
  position?: gsap.Position
961
1142
  ) => { duration: number }
962
- /** One soft glow pulse as a beat lands — light, not opacity. */
1143
+ /** One soft glow pulse as a beat lands — light, not opacity. */
963
1144
  glow: (target: string | Element, opts?: HighlightOpts, position?: gsap.Position) => { duration: number }
964
- /** Inverse move: everything EXCEPT the target dims behind a feathered
965
- * cutout. The strongest "look here" in the vocabulary. */
1145
+ /** Inverse move: everything EXCEPT the target dims behind a feathered
1146
+ * cutout. The strongest "look here" in the vocabulary. */
966
1147
  spotlight: (target: string | Element, opts?: SpotlightOpts, position?: gsap.Position) => { duration: number }
967
1148
  }
968
1149
 
969
1150
  // ─── Looks — scene light, shading, backdrops, hero shot, track ───────────
970
1151
 
971
1152
  export type LightState = {
972
- /** Direction the light COMES FROM, degrees clockwise from 12 o'clock.
973
- * 135 = upper-left key (the portrait default). */
1153
+ /** Direction the light COMES FROM, degrees clockwise from 12 o'clock.
1154
+ * 135 = upper-left key (the portrait default). */
974
1155
  angle: number
975
- /** 0 cool … 1 warm. Tints sweeps and rims. */
1156
+ /** 0 cool … 1 warm. Tints sweeps and rims. */
976
1157
  warmth: number
977
1158
  }
978
1159
 
979
1160
  export type LightKit = {
980
- /** Declare the scene light. Call once, early; sweeps, rims, and cast
981
- * shadows derive their direction from it. */
1161
+ /** Declare the scene light. Call once, early; sweeps, rims, and cast
1162
+ * shadows derive their direction from it. */
982
1163
  set: (state: Partial<LightState>) => void
983
- /** A glint sweeps across the element, angled with the scene light — the
984
- * `logo.shine` move generalized to any surface. */
1164
+ /** A glint sweeps across the element, angled with the scene light — the
1165
+ * `logo.shine` move generalized to any surface. */
985
1166
  sweep: (
986
1167
  target: string | Element,
987
1168
  opts?: { duration?: number; intensity?: number; width?: number; ease?: string },
988
1169
  position?: gsap.Position
989
1170
  ) => { duration: number }
990
- /** Edge light on the side facing the scene light — the premium product-
991
- * shot rim. Fades in and holds. */
1171
+ /** Edge light on the side facing the scene light — the premium product-
1172
+ * shot rim. Fades in and holds. */
992
1173
  rim: (
993
1174
  target: string | Element,
994
1175
  opts?: { color?: string; intensity?: number; duration?: number },
@@ -997,22 +1178,22 @@ export type LightKit = {
997
1178
  }
998
1179
 
999
1180
  export type ShadeKit = {
1000
- /** Soft shadow under the element, offset along the scene light. Replaces
1001
- * hand-tuned boxShadow strings and always agrees with every other shadow. */
1181
+ /** Soft shadow under the element, offset along the scene light. Replaces
1182
+ * hand-tuned boxShadow strings and always agrees with every other shadow. */
1002
1183
  cast: (
1003
1184
  target: string | Element,
1004
1185
  opts?: { distance?: number; softness?: number; opacity?: number; duration?: number },
1005
1186
  position?: gsap.Position
1006
1187
  ) => { duration: number }
1007
- /** Frame vignette, faded in once and held (like the grain plate — never
1008
- * cut, so it doesn't blink at hard cuts). */
1188
+ /** Frame vignette, faded in once and held (like the grain plate — never
1189
+ * cut, so it doesn't blink at hard cuts). */
1009
1190
  vignette: (
1010
1191
  opts?: { strength?: number; duration?: number },
1011
1192
  position?: gsap.Position
1012
1193
  ) => { duration: number }
1013
- /** Named color grade on the scene content. 'warm-product' | 'cool-saas' |
1014
- * 'noir'. Applied to the scene root, so overlays (cursors, highlights)
1015
- * stay ungraded and legible. */
1194
+ /** Named color grade on the scene content. 'warm-product' | 'cool-saas' |
1195
+ * 'noir'. Applied to the scene root, so overlays (cursors, highlights)
1196
+ * stay ungraded and legible. */
1016
1197
  grade: (
1017
1198
  preset: 'warm-product' | 'cool-saas' | 'noir',
1018
1199
  opts?: { duration?: number },
@@ -1032,28 +1213,28 @@ export type BackdropStyle =
1032
1213
  | 'grid-glow'
1033
1214
 
1034
1215
  export type BackdropOpts = {
1035
- /** 0..1 — scales amplitude, speed, and opacity together. The default 0.3
1036
- * keeps the preset inside the sub-salient budget. */
1216
+ /** 0..1 — scales amplitude, speed, and opacity together. The default 0.3
1217
+ * keeps the preset inside the sub-salient budget. */
1037
1218
  energy?: number
1038
- /** Colors for the soft forms. Keep them near the scene's background
1039
- * luminance — the preset caps opacity, but hue contrast is yours. */
1219
+ /** Colors for the soft forms. Keep them near the scene's background
1220
+ * luminance — the preset caps opacity, but hue contrast is yours. */
1040
1221
  palette?: string[]
1041
- /** Base fill the backdrop paints under its forms. The backdrop mounts
1042
- * BEHIND the scene root, so a root that goes transparent needs the
1043
- * backdrop to own the ground color. */
1222
+ /** Base fill the backdrop paints under its forms. The backdrop mounts
1223
+ * BEHIND the scene root, so a root that goes transparent needs the
1224
+ * backdrop to own the ground color. */
1044
1225
  base?: string
1045
- /** FIXED BACKPLATE (default true): mount outside the camera-carrying
1046
- * stage, so zooms and pans move the UI plane while the ground stays put —
1047
- * the studio-cyclorama behavior of reference launch films. Set false to
1048
- * make the backdrop part of the world the camera moves through. */
1226
+ /** FIXED BACKPLATE (default true): mount outside the camera-carrying
1227
+ * stage, so zooms and pans move the UI plane while the ground stays put —
1228
+ * the studio-cyclorama behavior of reference launch films. Set false to
1229
+ * make the backdrop part of the world the camera moves through. */
1049
1230
  fixed?: boolean
1050
1231
  }
1051
1232
 
1052
1233
  export type BackdropHandle = {
1053
1234
  el: HTMLElement
1054
- /** Duck the backdrop under a dense foreground beat. */
1235
+ /** Duck the backdrop under a dense foreground beat. */
1055
1236
  calm: (position?: gsap.Position) => void
1056
- /** Bring it back up during a hold. */
1237
+ /** Bring it back up during a hold. */
1057
1238
  lift: (position?: gsap.Position) => void
1058
1239
  }
1059
1240
 
@@ -1062,26 +1243,26 @@ export type HeroAnchor =
1062
1243
  | { x: number; y: number }
1063
1244
 
1064
1245
  export type HeroOpts = {
1065
- /** Which region of the target the camera pushes toward. Default
1066
- * 'bottom-right' — the submit-button corner. */
1246
+ /** Which region of the target the camera pushes toward. Default
1247
+ * 'bottom-right' — the submit-button corner. */
1067
1248
  anchor?: HeroAnchor
1068
- /** How far in: scale ≈ (frame width / target width) × zoom. Default 1.35 —
1069
- * the target overflows the frame, close-up territory. */
1249
+ /** How far in: scale ≈ (frame width / target width) × zoom. Default 1.35 —
1250
+ * the target overflows the frame, close-up territory. */
1070
1251
  zoom?: number
1071
- /** Perspective tilt in degrees. Defaults derive from the anchor (the panel
1072
- * leans its far corner away): e.g. bottom-right → pitch 7, yaw -9. */
1252
+ /** Perspective tilt in degrees. Defaults derive from the anchor (the panel
1253
+ * leans its far corner away): e.g. bottom-right → pitch 7, yaw -9. */
1073
1254
  pitch?: number
1074
1255
  yaw?: number
1075
- /** Push-in time. Default 0.9. */
1256
+ /** Push-in time. Default 0.9. */
1076
1257
  duration?: number
1077
- /** Seconds held at the angle before returning. Default 1.2. */
1258
+ /** Seconds held at the angle before returning. Default 1.2. */
1078
1259
  hold?: number
1079
- /** Return time. Default 0.8; `settle: false` stays in the pose. */
1260
+ /** Return time. Default 0.8; `settle: false` stays in the pose. */
1080
1261
  returnDuration?: number
1081
1262
  settle?: boolean
1082
- /** Sheen sweep across the surface during the push. Default true. */
1263
+ /** Sheen sweep across the surface during the push. Default true. */
1083
1264
  sheen?: boolean
1084
- /** Shadow deepens as the panel lifts. Default true. */
1265
+ /** Shadow deepens as the panel lifts. Default true. */
1085
1266
  shadow?: boolean
1086
1267
  ease?: string
1087
1268
  }
@@ -1089,39 +1270,39 @@ export type HeroOpts = {
1089
1270
  export type LooksKit = {
1090
1271
  light: LightKit
1091
1272
  shade: ShadeKit
1092
- /** Mount an ambient animated backdrop behind the scene root. */
1273
+ /** Mount an ambient animated backdrop behind the scene root. */
1093
1274
  backdrop: (style: BackdropStyle, opts?: BackdropOpts) => BackdropHandle
1094
- /** The angle-zoom product shot: camera pushes toward one corner of the
1095
- * target while it tilts in perspective, sheen and shadow responding.
1096
- * A full round trip by default. */
1275
+ /** The angle-zoom product shot: camera pushes toward one corner of the
1276
+ * target while it tilts in perspective, sheen and shadow responding.
1277
+ * A full round trip by default. */
1097
1278
  hero: (target: string | Element, opts?: HeroOpts, position?: gsap.Position) => { duration: number }
1098
- /** Standalone perspective pose: tilt an element (not the stage) and hold.
1099
- * Tilt back with pitch/yaw 0. */
1279
+ /** Standalone perspective pose: tilt an element (not the stage) and hold.
1280
+ * Tilt back with pitch/yaw 0. */
1100
1281
  tilt: (
1101
1282
  target: string | Element,
1102
1283
  opts?: { pitch?: number; yaw?: number; duration?: number; ease?: string; perspective?: number },
1103
1284
  position?: gsap.Position
1104
1285
  ) => { duration: number }
1105
- /** Camera FOLLOWS an element while it moves: per-frame the tracked
1106
- * element's live position re-centers the camera at a locked occupancy.
1107
- * Seek-deterministic — each frame is a pure function of timeline position
1108
- * (the catch-up from `from` is an eased blend, not stateful smoothing).
1109
- * AUTHOR IT AFTER the motion it follows, so it renders after the movers. */
1286
+ /** Camera FOLLOWS an element while it moves: per-frame the tracked
1287
+ * element's live position re-centers the camera at a locked occupancy.
1288
+ * Seek-deterministic — each frame is a pure function of timeline position
1289
+ * (the catch-up from `from` is an eased blend, not stateful smoothing).
1290
+ * AUTHOR IT AFTER the motion it follows, so it renders after the movers. */
1110
1291
  track: (
1111
1292
  target: string | Element,
1112
1293
  opts?: {
1113
- /** Tracking window in seconds. Default 2. */
1294
+ /** Tracking window in seconds. Default 2. */
1114
1295
  duration?: number
1115
- /** Target occupancy as fraction of frame height. Default 0.5. */
1296
+ /** Target occupancy as fraction of frame height. Default 0.5. */
1116
1297
  height?: number
1117
- /** Fraction of the window spent easing from `from` onto the target.
1118
- * Default 0.25. */
1298
+ /** Fraction of the window spent easing from `from` onto the target.
1299
+ * Default 0.25. */
1119
1300
  attach?: number
1120
- /** Camera pose the track blends FROM (what the camera shows when the
1121
- * window opens). Default full frame. */
1301
+ /** Camera pose the track blends FROM (what the camera shows when the
1302
+ * window opens). Default full frame. */
1122
1303
  from?: { scale: number; x: number; y: number }
1123
- /** After the window: 'hold' keeps the last framing (a later camera call
1124
- * takes over); 'full' returns to the full frame. Default 'full'. */
1304
+ /** After the window: 'hold' keeps the last framing (a later camera call
1305
+ * takes over); 'full' returns to the full frame. Default 'full'. */
1125
1306
  release?: 'hold' | 'full'
1126
1307
  },
1127
1308
  position?: gsap.Position
@@ -1142,85 +1323,129 @@ export type SentenceHandle = {
1142
1323
  // ─── Morph — any element becomes any other element ───────────────────────
1143
1324
 
1144
1325
  export type MorphOpts = {
1145
- /** 'apple' = a firm in-out flight that keeps the deformation READABLE —
1146
- * the shape change is the subject, so it may not be over in three frames.
1147
- * 'spring' = the same flight with the reference bounce: a LIGHT damped
1148
- * sine riding the same path, calibrated from the dnyxstudios AE expression
1149
- * (amp .04·v, freq 1.8Hz, decay e^-3t, eased envelope). One motion, no
1150
- * delayed settle, ~one visible overshoot.
1151
- *
1152
- * Both are `ease-in-out-*` by design. A morph is travel WITHIN the frame,
1153
- * and the house table reserves `ease-out-*` for entrances; a front-loaded
1154
- * curve finishes the transformation before the viewer can look at it. */
1326
+ /** 'apple' = a firm in-out flight that keeps the deformation READABLE —
1327
+ * the shape change is the subject, so it may not be over in three frames.
1328
+ * 'spring' = the same flight with the reference bounce: a LIGHT damped
1329
+ * sine riding the same path, calibrated from the dnyxstudios AE expression
1330
+ * (amp .04·v, freq 1.8Hz, decay e^-3t, eased envelope). One motion, no
1331
+ * delayed settle, ~one visible overshoot.
1332
+ *
1333
+ * Both are `ease-in-out-*` by design. A morph is travel WITHIN the frame,
1334
+ * and the house table reserves `ease-out-*` for entrances; a front-loaded
1335
+ * curve finishes the transformation before the viewer can look at it. */
1155
1336
  feel?: 'apple' | 'spring'
1156
- /** Tune the 'spring' bounce. `amp` = first overshoot as a fraction of the
1157
- * travel (default 0.025 — light), `freq` Hz (default 1.8), `decay` per
1158
- * second (default 3), `flight` = fraction of `duration` spent on the main
1159
- * move before the bounce tail (default 0.55). */
1337
+ /** Tune the 'spring' bounce. `amp` = first overshoot as a fraction of the
1338
+ * travel (default 0.025 — light), `freq` Hz (default 1.8), `decay` per
1339
+ * second (default 3), `flight` = fraction of `duration` spent on the main
1340
+ * move before the bounce tail (default 0.55). */
1160
1341
  bounce?: { amp?: number; freq?: number; decay?: number; flight?: number }
1161
1342
  duration?: number
1162
- /** Flight curve — ANY GSAP ease works: house names, 'elastic.out(1, 0.4)',
1163
- * 'back.out(1.8)'… Overshoots extrapolate past the target (kept renderable). */
1343
+ /** Flight curve — ANY GSAP ease works: house names, 'elastic.out(1, 0.4)',
1344
+ * 'back.out(1.8)'… Overshoots extrapolate past the target (kept renderable). */
1164
1345
  ease?: string
1165
- /** 0..1 in EASED space — where along the SHAPE CHANGE the surface swaps
1166
- * from A to B. Not a fade midpoint: at this instant both sides hold the
1167
- * same box and the same chrome, so the swap is a match-frame cut. 0.5
1168
- * means "when the shape is halfway between the two," which is what the
1169
- * viewer actually perceives. */
1346
+ /** 0..1 in EASED space — where along the SHAPE CHANGE the surface swaps
1347
+ * from A to B. Not a fade midpoint: at this instant both sides hold the
1348
+ * same box and the same chrome, so the swap is a match-frame cut. 0.5
1349
+ * means "when the shape is halfway between the two," which is what the
1350
+ * viewer actually perceives. */
1170
1351
  handoff?: number
1171
- /** Fraction of the flight over which unmatched content departs and
1172
- * arrives (default 0.42). Content displaces along the travel vector and
1173
- * must be clear of the seam — that emptiness is what makes the cut
1174
- * invisible, so widening this past ~0.5 will start to show the swap. */
1352
+ /** Fraction of the flight over which unmatched content departs and
1353
+ * arrives (default 0.42). Content displaces along the travel vector and
1354
+ * must be clear of the seam — that emptiness is what makes the cut
1355
+ * invisible, so widening this past ~0.5 will start to show the swap. */
1175
1356
  content?: number
1176
- /** Arc lift in px (0 disables). Default: auto from travel distance. */
1357
+ /** Arc lift in px (0 disables). Default: auto from travel distance. */
1177
1358
  arc?: number
1178
- /** Content choreography (default 'displace'): unmatched children leave and
1179
- * arrive by moving ALONG the travel vector — replacement reads as motion,
1180
- * and both sides are clear of the seam before the surfaces swap.
1181
- * `false` = containers only; children hold still and stay counter-scaled,
1182
- * which is right when the two sides share a background image or the
1183
- * content is meant to look continuous. */
1359
+ /** Content choreography (default 'displace'): unmatched children leave and
1360
+ * arrive by moving ALONG the travel vector — replacement reads as motion,
1361
+ * and both sides are clear of the seam before the surfaces swap.
1362
+ * `false` = containers only; children hold still and stay counter-scaled,
1363
+ * which is right when the two sides share a background image or the
1364
+ * content is meant to look continuous. */
1184
1365
  parts?: 'displace' | false
1185
- /** Sub-component matching (default 'auto'): elements tagged
1186
- * `data-morph="name"` on BOTH sides (any depth) morph INTO each other —
1187
- * each pair rides its own interpolated box inside the flight, so the
1188
- * source's sub-component visibly becomes the target's. Untagged direct
1189
- * children are greedily auto-paired by text overlap. `false` disables. */
1366
+ /** Sub-component matching (default 'auto'): elements tagged
1367
+ * `data-morph="name"` on BOTH sides (any depth) morph INTO each other —
1368
+ * each pair rides its own interpolated box inside the flight, so the
1369
+ * source's sub-component visibly becomes the target's. Untagged direct
1370
+ * children are greedily auto-paired by text overlap. `false` disables. */
1190
1371
  match?: 'auto' | false
1191
- /** Landing overshoot on top of a non-spring flight. `true`/default object
1192
- * for the soft house settle, `{ scale, duration, ease }` to tune (elastic
1193
- * landings live here), `false` for none. 'spring' feel ignores this — its
1194
- * ring is already in the flight. */
1372
+ /** Landing overshoot on top of a non-spring flight. `true`/default object
1373
+ * for the soft house settle, `{ scale, duration, ease }` to tune (elastic
1374
+ * landings live here), `false` for none. 'spring' feel ignores this — its
1375
+ * ring is already in the flight. */
1195
1376
  settle?: boolean | { scale?: number; duration?: number; ease?: string }
1196
1377
  zIndex?: number
1197
1378
  }
1198
1379
 
1380
+ // ─── Morph surfaces — continuous vector material and DOM continuity ──────
1381
+
1382
+ export type MorphSurfaceLayer = {
1383
+ from: string; to: string; color?: string; endColor?: string
1384
+ stroke?: number; opacity?: number; endOpacity?: number
1385
+ }
1386
+
1387
+ export type MorphSurfaceOpts = MorphTreeOpts & {
1388
+ layers: MorphSurfaceLayer[]
1389
+ /** Coordinates of the authored vector surface; roots provide its CSS size. */
1390
+ viewBox?: string
1391
+ /** Keep decoration inside the changing outer shape; null disables clipping. */
1392
+ clipToLayer?: number | null
1393
+ }
1394
+
1395
+ export type MorphSurfaceResult = MorphTreeResult & { surfaceLayers: number }
1396
+
1397
+ // ─── Morph tree — recursive continuity ───────────────────────────────────
1398
+
1399
+ export type MorphTreeOpts = {
1400
+ duration?: number
1401
+ ease?: string
1402
+ /** How deep to recurse before children ride their parent rigidly. */
1403
+ maxDepth?: number
1404
+ /** Perceptual budget: how many nodes may move independently. Past ~6 the
1405
+ * eye stops reading structure and starts seeing a swarm. */
1406
+ maxMoving?: number
1407
+ /** Minimum affinity to call two elements the same thing. Lower = more
1408
+ * matches and more motion; higher = more collapse-and-emerge. */
1409
+ floor?: number
1410
+ /** Fraction of the flight each depth level waits before it resolves.
1411
+ * Parents commit first; children follow. 0 disables the cascade. */
1412
+ lead?: number
1413
+ /** Gather N sources into one target when the counts are lopsided. */
1414
+ cluster?: boolean
1415
+ arc?: number
1416
+ }
1417
+
1418
+ export type MorphTreeResult = {
1419
+ duration: number
1420
+ moving: number
1421
+ debug: string
1422
+ }
1423
+
1199
1424
  // ─── Engine — the direction context you write against ────────────────────
1200
1425
 
1201
1426
  /** Render directives a scene (or a kit, during the direction) hands to the
1202
- * exporter. The channel already exists end to end: `export const output` in
1203
- * direction.ts → loadScene → buildScene → window.__config → the exporter's
1204
- * probe — and because __config is assigned AFTER the direction runs, a kit
1205
- * can set these from inside a direction call. Everything optional; absent
1206
- * means today's behaviour exactly. */
1427
+ * exporter. The channel already exists end to end: `export const output` in
1428
+ * direction.ts → loadScene → buildScene → window.__config → the exporter's
1429
+ * probe — and because __config is assigned AFTER the direction runs, a kit
1430
+ * can set these from inside a direction call. Everything optional; absent
1431
+ * means today's behaviour exactly. */
1207
1432
  export type RenderConfig = {
1208
- /** Temporal motion blur (Phase 6). `off` (default) renders one sample per
1209
- * frame. Tiers fix samples-per-frame; `runs` limits blur to intervals so a
1210
- * 30s clip with 6s of camera moves does not pay the multiplier throughout. */
1433
+ /** Temporal motion blur (Phase 6). `off` (default) renders one sample per
1434
+ * frame. Tiers fix samples-per-frame; `runs` limits blur to intervals so a
1435
+ * 30s clip with 6s of camera moves does not pay the multiplier throughout. */
1211
1436
  blur?: {
1212
1437
  tier?: 'off' | 'light' | 'full'
1213
- /** Shutter angle in degrees, 0–360. Default 180. Costs nothing — it only
1214
- * changes WHICH times get sampled, never how many. */
1438
+ /** Shutter angle in degrees, 0–360. Default 180. Costs nothing — it only
1439
+ * changes WHICH times get sampled, never how many. */
1215
1440
  shutter?: number
1216
- /** Blur only these intervals, in seconds. Absent = the whole timeline. */
1441
+ /** Blur only these intervals, in seconds. Absent = the whole timeline. */
1217
1442
  runs?: Array<{ from: number; to: number }>
1218
1443
  }
1219
1444
  }
1220
1445
 
1221
1446
  /** Committed output of scripts/analyze-music.mjs — one JSON per track in
1222
- * music/analysis/. A static file, identical in every export shard, which is
1223
- * what makes audio-reactive motion compatible with seek-anywhere rendering. */
1447
+ * music/analysis/. A static file, identical in every export shard, which is
1448
+ * what makes audio-reactive motion compatible with seek-anywhere rendering. */
1224
1449
  export type TrackAnalysis = {
1225
1450
  analyzerVersion: number
1226
1451
  rate: number // envelope grid Hz
@@ -1236,25 +1461,25 @@ export type TrackAnalysis = {
1236
1461
  }
1237
1462
 
1238
1463
  export type AudioKit = {
1239
- /** True when a real analysis file was loaded. When false every envelope
1240
- * reads 0 and the beat grid is synthetic — directions never throw. */
1464
+ /** True when a real analysis file was loaded. When false every envelope
1465
+ * reads 0 and the beat grid is synthetic — directions never throw. */
1241
1466
  live: boolean
1242
1467
  bpm: number
1243
1468
  beats: number[]
1244
1469
  downbeats: number[]
1245
1470
  onsets: number[]
1246
1471
  sections: Array<{ start: number; end: number; energy: number }>
1247
- /** Overall loudness at timeline time t, 0..1. Pure lookup — the exporter
1248
- * loops the track under a longer video, so t wraps at the track length. */
1472
+ /** Overall loudness at timeline time t, 0..1. Pure lookup — the exporter
1473
+ * loops the track under a longer video, so t wraps at the track length. */
1249
1474
  energy: (t: number) => number
1250
1475
  band: (t: number, band: 'low' | 'mid' | 'high') => number
1251
- /** Timeline time of beat n (downbeat-phase corrected). */
1476
+ /** Timeline time of beat n (downbeat-phase corrected). */
1252
1477
  beat: (n: number) => number
1253
- /** Map the energy envelope onto element properties: one proxy tween whose
1254
- * onUpdate reads energy(t) — a pure function of playhead, so it renders
1255
- * identically from any seek. `map` receives 0..1 and returns gsap vars to
1256
- * set, e.g. (e) => ({ scale: 1 + e * 0.06 }). NO smoothing at read time:
1257
- * an IIR filter over successive frames is the shard-seam bug. */
1478
+ /** Map the energy envelope onto element properties: one proxy tween whose
1479
+ * onUpdate reads energy(t) — a pure function of playhead, so it renders
1480
+ * identically from any seek. `map` receives 0..1 and returns gsap vars to
1481
+ * set, e.g. (e) => ({ scale: 1 + e * 0.06 }). NO smoothing at read time:
1482
+ * an IIR filter over successive frames is the shard-seam bug. */
1258
1483
  drive: (
1259
1484
  target: string | Element,
1260
1485
  map: (energy: number) => Record<string, number | string>,
@@ -1264,7 +1489,7 @@ export type AudioKit = {
1264
1489
  }
1265
1490
 
1266
1491
  export type Output = {
1267
- /** Delivery raster dimensions. They do not change scene layout. */
1492
+ /** Delivery raster dimensions. They do not change scene layout. */
1268
1493
  width: number
1269
1494
  height: number
1270
1495
  fps: number
@@ -1272,7 +1497,7 @@ export type Output = {
1272
1497
  }
1273
1498
 
1274
1499
  /** Resolved renderer contract. `width`/`height` are always logical scene
1275
- * coordinates; only the exporter consumes `delivery` as raster dimensions. */
1500
+ * coordinates; only the exporter consumes `delivery` as raster dimensions. */
1276
1501
  export type StageConfig = Output & {
1277
1502
  composition: { width: number; height: number }
1278
1503
  delivery: { width: number; height: number }
@@ -1285,21 +1510,21 @@ export type FramingTarget = string | Element | Rect | FramingTarget[]
1285
1510
  export type Framing = { scale: number; x: number; y: number }
1286
1511
 
1287
1512
  export type CameraFocusOpts = {
1288
- /** Fraction of the composition height occupied by the subject. Default 0.7. */
1513
+ /** Fraction of the composition height occupied by the subject. Default 0.7. */
1289
1514
  height?: number
1290
- /** `contain` also caps width; `height` guarantees height and may crop horizontally. */
1515
+ /** `contain` also caps width; `height` guarantees height and may crop horizontally. */
1291
1516
  mode?: 'contain' | 'height'
1292
- /** Width cap used by `contain`. Default 0.9. */
1517
+ /** Width cap used by `contain`. Default 0.9. */
1293
1518
  maxWidth?: number
1294
- /** Optional safety ceiling. When hit, exact occupancy cannot be guaranteed. */
1519
+ /** Optional safety ceiling. When hit, exact occupancy cannot be guaranteed. */
1295
1520
  maxScale?: number
1296
1521
  offsetX?: number
1297
1522
  offsetY?: number
1298
1523
  clamp?: boolean
1299
- /** Camera travel time. Default 0.8 seconds. */
1524
+ /** Camera travel time. Default 0.8 seconds. */
1300
1525
  duration?: number
1301
- /** Camera travel ease. Defaults to the house dolly (`expo.inOut`); pass one
1302
- * only when the user explicitly asks for a different camera feel. */
1526
+ /** Camera travel ease. Defaults to the house dolly (`expo.inOut`); pass one
1527
+ * only when the user explicitly asks for a different camera feel. */
1303
1528
  ease?: string
1304
1529
  }
1305
1530
 
@@ -1316,12 +1541,12 @@ export type FramingOpts = {
1316
1541
  export type TypeOpts = {
1317
1542
  cps?: number // characters per second
1318
1543
  caret?: boolean
1319
- /** Opt-in invisible tracking head that glides smoothly along the typed
1320
- * text — constant speed within each visual line, a quick eased drop at
1321
- * every wrap. Hand it to `track()` or a camera framing; the caret itself
1322
- * steps per character and wraps, so never track the caret directly.
1323
- * `width` is the head's scene-px width (default 300); `height` is a
1324
- * multiple of the line height (default 2.9). */
1544
+ /** Opt-in invisible tracking head that glides smoothly along the typed
1545
+ * text — constant speed within each visual line, a quick eased drop at
1546
+ * every wrap. Hand it to `track()` or a camera framing; the caret itself
1547
+ * steps per character and wraps, so never track the caret directly.
1548
+ * `width` is the head's scene-px width (default 300); `height` is a
1549
+ * multiple of the line height (default 2.9). */
1325
1550
  head?: boolean | { width?: number; height?: number }
1326
1551
  }
1327
1552
 
@@ -1342,8 +1567,8 @@ export type TextStyleOverride = {
1342
1567
  color?: string
1343
1568
  x?: number
1344
1569
  y?: number
1345
- /** Wrapping width in scene px — the side bars reflow text instead of
1346
- * scale-squeezing it. Applied as inline-block + width. */
1570
+ /** Wrapping width in scene px — the side bars reflow text instead of
1571
+ * scale-squeezing it. Applied as inline-block + width. */
1347
1572
  width?: number
1348
1573
  hidden?: boolean
1349
1574
  }
@@ -1355,13 +1580,13 @@ export type ObjectPositionOverride = {
1355
1580
  scaleY?: number
1356
1581
  rotation?: number
1357
1582
  hidden?: boolean
1358
- /** Plain CSS declarations the studio applies to the object's element —
1359
- * border-radius, background-color, box-shadow and friends. Applied at
1360
- * stage prepare, so they ride the structural rebuild route. */
1583
+ /** Plain CSS declarations the studio applies to the object's element —
1584
+ * border-radius, background-color, box-shadow and friends. Applied at
1585
+ * stage prepare, so they ride the structural rebuild route. */
1361
1586
  styles?: Record<string, string>
1362
- /** Replacement source for the image or video the object carries (the
1363
- * element itself or its first img/video descendant), usually a data URI so
1364
- * renders never depend on the network. */
1587
+ /** Replacement source for the image or video the object carries (the
1588
+ * element itself or its first img/video descendant), usually a data URI so
1589
+ * renders never depend on the network. */
1365
1590
  src?: string
1366
1591
  }
1367
1592
 
@@ -1379,10 +1604,10 @@ export type SceneOverrides = {
1379
1604
  textStyles?: Record<string, TextStyleOverride>
1380
1605
  addedText?: Record<string, AddedTextLayer>
1381
1606
  objects?: Record<string, ObjectPositionOverride> // stable UI wrapper id -> scene-pixel offset
1382
- /** Explicit paint order for editable siblings. Keys include the editable
1383
- * kind (`object:…` / `text:…`) so independently generated ids cannot
1384
- * collide. Values are normalized by the studio whenever an item moves in
1385
- * the stack. */
1607
+ /** Explicit paint order for editable siblings. Keys include the editable
1608
+ * kind (`object:…` / `text:…`) so independently generated ids cannot
1609
+ * collide. Values are normalized by the studio whenever an item moves in
1610
+ * the stack. */
1386
1611
  layerOrder?: Record<string, number>
1387
1612
  }
1388
1613
 
@@ -1402,6 +1627,54 @@ export type TweenKind =
1402
1627
  | 'axis'
1403
1628
  | 'three'
1404
1629
 
1630
+ export type EaseFingerprint = [number, number, number, number]
1631
+
1632
+ export type EaseResolutionInfo = {
1633
+ requested: string
1634
+ resolvedId: string
1635
+ status: 'default' | 'resolved' | 'custom' | 'unresolved'
1636
+ resolvedFingerprint: EaseFingerprint
1637
+ }
1638
+
1639
+ export type MotionOperationInfo = {
1640
+ operationId: string
1641
+ primitive: string
1642
+ target?: string
1643
+ targetCount?: number
1644
+ requestedOptionKeys: string[]
1645
+ acceptedOptionKeys: string[]
1646
+ droppedOptionKeys: string[]
1647
+ ease?: EaseResolutionInfo
1648
+ effective?: {
1649
+ transition?: {
1650
+ style: string
1651
+ direction: string
1652
+ color: string
1653
+ duration: number
1654
+ at: { x: number; y: number }
1655
+ }
1656
+ blur?: {
1657
+ from: number
1658
+ to: number
1659
+ tier: string
1660
+ samples: number
1661
+ shutter: number
1662
+ }
1663
+ }
1664
+ }
1665
+
1666
+ export type ExecutionDiagnostic = {
1667
+ code: 'unresolved-ease' | 'unknown-option-key' | 'dead-selector'
1668
+ operationId: string
1669
+ primitive: string
1670
+ message: string
1671
+ }
1672
+
1673
+ export type ExecutionIntegrityInspection = {
1674
+ operations: MotionOperationInfo[]
1675
+ diagnostics: ExecutionDiagnostic[]
1676
+ }
1677
+
1405
1678
  export type TweenInfo = {
1406
1679
  id: number
1407
1680
  sig: string
@@ -1411,11 +1684,17 @@ export type TweenInfo = {
1411
1684
  duration: number // per-target duration (excludes stagger span)
1412
1685
  total: number // full span including stagger
1413
1686
  ease: string // '' = gsap default
1687
+ target: string
1688
+ targetCount: number
1689
+ easeRequested: string
1690
+ easeResolvedId: string
1691
+ easeResolutionStatus: 'default' | 'resolved' | 'custom' | 'unresolved'
1692
+ easeFingerprint: EaseFingerprint
1414
1693
  stagger?: number
1415
1694
  vars: Record<string, number | string>
1416
1695
  overridden: boolean
1417
- /** The editable layer this tween animates, when its first Element target
1418
- * sits inside one — lets the studio show a layer's choreography. */
1696
+ /** The editable layer this tween animates, when its first Element target
1697
+ * sits inside one — lets the studio show a layer's choreography. */
1419
1698
  targetObjectId?: string
1420
1699
  targetTextId?: string
1421
1700
  }
@@ -1428,14 +1707,50 @@ export type SceneInspection = {
1428
1707
  tweens: TweenInfo[]
1429
1708
  cameraSets: CameraSetInfo[]
1430
1709
  overrides: SceneOverrides
1431
- /**
1432
- * Non-fatal build complaints — a step whose element is gone, a text effect
1433
- * with nothing left to split. The scene still plays; these say what silently
1434
- * does nothing so a stale selector can't rot unnoticed.
1435
- */
1710
+ /**
1711
+ * Non-fatal build complaints — a step whose element is gone, a text effect
1712
+ * with nothing left to split. The scene still plays; these say what silently
1713
+ * does nothing so a stale selector can't rot unnoticed.
1714
+ */
1436
1715
  warnings: string[]
1437
- }
1438
-
1716
+ /** Observational diagnostics; never rejects or changes authored motion. */
1717
+ execution: ExecutionIntegrityInspection
1718
+ }
1719
+
1720
+ /**
1721
+ * The context a direction is written against.
1722
+ *
1723
+ * HOW A FILM IS WRITTEN. `createFilmSequencer` (kit/film) owns semantic
1724
+ * ordering. `createWorldCamera` (kit/world) owns named-object framing, depth,
1725
+ * and camera gestures—never content animation or editorial rhythm:
1726
+ *
1727
+ * const film = createFilmSequencer({ tl })
1728
+ * const world = createWorldCamera({ tl, stage, camera, register: 'promo' })
1729
+ * const copy = createTextPlans(text)
1730
+ * const result = film.do(manner.realize('[data-obj="RESULT_ROWS"]'))
1731
+ * film.do(world.camera.whipTo('RESULT_ROWS').landWith(result))
1732
+ * film.do(copy.exit('[data-claim]', 'rise'))
1733
+ * film.cut(editorial.cut(layers, next), world.camera.cutTo('NEXT'))
1734
+ *
1735
+ * Camera verbs are `cutTo` / `moveTo` / `zoomTo` / `whipTo` / `departTo` /
1736
+ * `angleTo` / `focusOn`. Every verb centres the named data-obj bounding box
1737
+ * and maximizes it inside a padded frame. The optional framing words are
1738
+ * only `'tight'` | `'normal'` | `'loose'`; they change breathing room, never
1739
+ * select or cap magnification. `film.do` advances to settle,
1740
+ * `film.together` shares a start, and
1741
+ * `film.at(handle.impact)` attaches a consequence to its cause. Any existing
1742
+ * kit call or bespoke GSAP gesture can be lifted with `film.plan()`. Primary
1743
+ * actions sequence from SETTLE; `film.cut()` uses the stricter END horizon, so
1744
+ * a scene can never hide an unfinished stagger or exit. Only
1745
+ * `film.coveredCut(handle, marker, reason, ...)` may cross that horizon, and it
1746
+ * makes the carrying motion explicit.
1747
+ *
1748
+ * The raw camera below (`framing` / `full` / `crop` / `set` / `move`) is the
1749
+ * ESCAPE HATCH, not the default: measured or skeleton work that needs an exact
1750
+ * pose at an exact time, and anything the kit has no vocabulary for. Reach for
1751
+ * it deliberately; a film that reaches for it by habit is composing shot by
1752
+ * shot with no subject, no containment and no continuity.
1753
+ */
1439
1754
  export type DirectionContext = {
1440
1755
  gsap: typeof gsap
1441
1756
  tl: gsap.core.Timeline // the master timeline (created paused)
@@ -1443,6 +1758,11 @@ export type DirectionContext = {
1443
1758
  config: StageConfig
1444
1759
  q: <T extends HTMLElement>(selector: string) => T
1445
1760
  qa: (selector: string) => HTMLElement[]
1761
+ /** The RAW camera: an exact pose at an exact time. Prefer World Camera's
1762
+ * named verbs for shots (see the note on `DirectionContext` above) — they
1763
+ * maximize and centre the named subject with deterministic padding, which
1764
+ * none of the calls here do for you. Use these for measured/skeleton work,
1765
+ * and inside kits. */
1446
1766
  camera: {
1447
1767
  framing: (target: FramingTarget, opts?: FramingOpts) => Framing
1448
1768
  focus: (
@@ -1451,13 +1771,18 @@ export type DirectionContext = {
1451
1771
  position?: gsap.Position
1452
1772
  ) => Framing
1453
1773
  full: () => Framing
1454
- /** CROP framing: the reference resting shot — the target deliberately
1455
- * OVERFLOWS the frame (`fill` × cover, default 1.4) and one side hugs a
1456
- * frame edge (`anchor`: 'left' | 'right' | 'top' | 'bottom' | corners |
1457
- * {x,y} fraction), the rest bleeding off. Returns a Framing: compose
1458
- * with camera.set for the CUT-TO-CROP edit (wide→detail is a cut) or
1459
- * camera.move for a motivated push. A full-frame centered app is an
1460
- * establishing shot, used once — proof shots default to crop. */
1774
+ /** CROP framing, the raw-camera close shot: the target deliberately
1775
+ * OVERFLOWS the frame (`fill` × cover, default 1.4) and one side hugs a
1776
+ * frame edge (`anchor`: 'left' | 'right' | 'top' | 'bottom' | corners |
1777
+ * {x,y} fraction), the rest bleeding off. Returns a Framing you compose
1778
+ * with camera.set (the cut) or camera.move (the motivated push).
1779
+ *
1780
+ * IN A WORLD-KIT FILM YOU DO NOT CALL THIS. The declared equivalents are
1781
+ * `fit: 'closeup'` (cropping IS the shot — it goes past the edge-safety
1782
+ * wall on a subject that declares `data-detail="fine"`) and `fit: 'wide'`
1783
+ * for the whole-product establishing shot, and the kit centres the named
1784
+ * subject instead of anchoring it to a frame edge by hand. Use `crop`
1785
+ * when you are hand-composing a pose the kit has no vocabulary for. */
1461
1786
  crop: (
1462
1787
  target: FramingTarget,
1463
1788
  opts?: {
@@ -1470,338 +1795,346 @@ export type DirectionContext = {
1470
1795
  }
1471
1796
  ) => Framing
1472
1797
  set: (framing: Framing) => void
1473
- /** The house dolly. DEFAULTS: `duration: 0.8`, `ease: 'expo.inOut'` —
1474
- * a long dwell at both ends and a confident glide through the middle.
1475
- * These defaults ARE the recommended camera physics: do NOT pass ease
1476
- * or duration here unless the user explicitly asks for a different
1477
- * camera feel. The one sanctioned exception: an instant reframe behind
1478
- * a covering element (`duration: 0.01`). A slow linear drift
1479
- * (`duration: 2–5, ease: 'none'`) is NOT one — sub-pixel motion on the
1480
- * whole stage re-rasterizes every glyph each frame and renders as
1481
- * micro-flicker; the motion lab flags it as DRIFT and export blocks.
1482
- * Hold the camera still instead. Anything between 0.05–0.4s is
1483
- * neither a move nor a cut — avoid it. `ease-in-out-cubic` is for
1484
- * ELEMENT travel, never the camera. */
1798
+ /** The house dolly. DEFAULTS: `duration: 0.8`, `ease: 'expo.inOut'` —
1799
+ * a long dwell at both ends and a confident glide through the middle.
1800
+ * These defaults ARE the recommended camera physics: do NOT pass ease
1801
+ * or duration here unless the user explicitly asks for a different
1802
+ * camera feel. The one sanctioned exception: an instant reframe behind
1803
+ * a covering element (`duration: 0.01`). A slow linear drift
1804
+ * (`duration: 2–5, ease: 'none'`) is NOT one — sub-pixel motion on the
1805
+ * whole stage re-rasterizes every glyph each frame and renders as
1806
+ * micro-flicker; the motion lab flags it as DRIFT and export blocks.
1807
+ * Hold the camera still instead. Anything between 0.05–0.4s is
1808
+ * neither a move nor a cut — avoid it. `ease-in-out-cubic` is for
1809
+ * ELEMENT travel, never the camera.
1810
+ *
1811
+ * A world-kit beat supplies all of this from its declared register, and
1812
+ * `camera: { cut: true }` is how a beat asks for the snap instead of the
1813
+ * glide — so a film built on the kit never writes a camera duration or
1814
+ * ease at all. */
1485
1815
  move: (
1486
1816
  framing: Framing,
1487
1817
  vars?: gsap.TweenVars,
1488
1818
  position?: gsap.Position
1489
1819
  ) => void
1490
1820
  }
1491
- /** Deterministic, seek-driven typing with a fake caret. The caret follows
1492
- * real layout (mirror-probed), so it wraps with the text in a textarea.
1493
- * `head` (opt-in) is a smooth glide marker for camera tracking. */
1821
+ /** Deterministic, seek-driven typing with a fake caret. The caret follows
1822
+ * real layout (mirror-probed), so it wraps with the text in a textarea.
1823
+ * `head` (opt-in) is a smooth glide marker for camera tracking. */
1494
1824
  typeInto: (
1495
1825
  target: string | HTMLTextAreaElement | HTMLInputElement,
1496
1826
  text: string,
1497
1827
  opts?: TypeOpts,
1498
1828
  position?: gsap.Position
1499
1829
  ) => { duration: number; caret: HTMLElement | null; head: HTMLElement | null }
1500
- /** Rect the finished text will occupy in `target` (for framing the close-up). */
1830
+ /** Rect the finished text will occupy in `target` (for framing the close-up). */
1501
1831
  measureTypedLine: (
1502
1832
  target: string | HTMLTextAreaElement | HTMLInputElement,
1503
1833
  text: string
1504
1834
  ) => Rect
1505
- /** Rect of an element's rendered text content (a block element's box
1506
- * spans its full column — this measures just the text). */
1835
+ /** Rect of an element's rendered text content (a block element's box
1836
+ * spans its full column — this measures just the text). */
1507
1837
  textRect: (target: string | Element) => Rect
1508
- /** gsap SplitText — animate .chars / .words / .lines. */
1838
+ /** gsap SplitText — animate .chars / .words / .lines. */
1509
1839
  split: (
1510
1840
  target: string | Element,
1511
1841
  vars?: SplitText.Vars
1512
1842
  ) => SplitText
1513
- /** Empty tween that extends the timeline (a beat / settle). */
1843
+ /** Empty tween that extends the timeline (a beat / settle). */
1514
1844
  hold: (duration: number, position?: gsap.Position) => void
1515
- /** Timeline-driven MP4/WebM playback: scrubs the element's currentTime,
1516
- * so scrubbing and export frames are exact (a free-running <video> would
1517
- * shimmer — never autoplay one). `from` = source offset in seconds;
1518
- * `duration` defaults to the rest of the source. Returns `{ duration }`.
1519
- * The element is muted and paused; exports wait for each frame's decode. */
1845
+ /** Timeline-driven MP4/WebM playback: scrubs the element's currentTime,
1846
+ * so scrubbing and export frames are exact (a free-running <video> would
1847
+ * shimmer — never autoplay one). `from` = source offset in seconds;
1848
+ * `duration` defaults to the rest of the source. Returns `{ duration }`.
1849
+ * The element is muted and paused; exports wait for each frame's decode. */
1520
1850
  video: (
1521
1851
  target: string | HTMLVideoElement,
1522
1852
  opts?: { from?: number; duration?: number; playbackRate?: number },
1523
1853
  position?: gsap.Position
1524
1854
  ) => { duration: number }
1525
- /** Named text animations (built on SplitText, all deterministic).
1526
- * `text.reveal(el, effect)` in: rise, fall, blur, wave, flip, pop, slide,
1527
- * typewriter, scramble, wipe, bounce, elastic, arc, streak. `text.exit(el,
1528
- * effect)` out: rise, fall, blur, fade, pop, scramble, wipe, scatter,
1529
- * streak. `text.count(el, { to, from?, duration? })` counts a number up on
1530
- * the timeline (stat pills). `text.emphasize(el, 'pulse'|'wiggle'|'shake')`
1531
- * is a settle-back gesture for CTAs/accent words. `text.underline(el)`
1532
- * draws a sketched stroke under a tight element.
1533
- *
1534
- * `transition.out(style)` / `transition.in(style)` cover/reveal the whole
1535
- * FRAME with a color (overlay outside the camera): styles 'wipe',
1536
- * 'pixels', 'iris', opts `{ color?, direction?: 'left'|'right'|'up'|
1537
- * 'down', duration?, cell?, at? }`. `out` with no position appends at the
1538
- * clip's end — the standard way to end a clip for a cut; `in` at position
1539
- * 0 starts the clip covered and reveals it.
1540
- * `transition.through(target, { via?, char?, word?, swap?, land?, color?,
1541
- * duration?, outDuration? }, position?)` is the GO-THROUGH: dive INTO
1542
- * something until its color takes over the frame, world-swap under the
1543
- * cover, pull out into the new world. `via` picks the dive geometry:
1544
- * 'surface' (default, the element's face), 'glyph' (`{ via: 'glyph',
1545
- * char: 'o' }` — through the character's enclosed counter, measured from
1546
- * the real glyph outline on plain text: no wrapper spans, no hand-tuned
1547
- * margins), 'word' (`{ via: 'word', word: 'world' }` — the word's rect),
1548
- * 'anchor' (match-cut: the subject persists across the cut — the same
1549
- * selector's twin in the new world is the landing — while everything
1550
- * around it changes). Never hand-roll a dive (a magic-scale camera.move +
1551
- * a hand-timed swap): through() computes the exact cover scale, measures
1552
- * at the dive's own start time, and guarantees full coverage at the cut.
1553
- * All return `{ duration }` (full span including
1554
- * stagger) for sequencing, and accept `(target, effect, opts?, position?)`
1555
- * with opts `{ by?: 'chars'|'words'|'lines', duration?, stagger?, ease?,
1556
- * from?: 'start'|'center'|'end'|'edges' }`. */
1855
+ /** Named text animations (built on SplitText, all deterministic).
1856
+ * `text.reveal(el, effect)` in: rise, fall, blur, wave, flip, pop, slide,
1857
+ * typewriter, scramble, wipe, bounce, elastic, arc, streak. `text.exit(el,
1858
+ * effect)` out: rise, fall, blur, fade, pop, scramble, wipe, scatter,
1859
+ * streak. `text.count(el, { to, from?, duration? })` counts a number up on
1860
+ * the timeline (stat pills). `text.emphasize(el, 'pulse'|'wiggle'|'shake')`
1861
+ * is a settle-back gesture for CTAs/accent words. `text.underline(el)`
1862
+ * draws a sketched stroke under a tight element.
1863
+ *
1864
+ * `transition.out(style)` / `transition.in(style)` cover/reveal the whole
1865
+ * FRAME with a color (overlay outside the camera): styles 'wipe',
1866
+ * 'pixels', 'iris', opts `{ color?, direction?: 'left'|'right'|'up'|
1867
+ * 'down', duration?, cell?, at? }`. `out` with no position appends at the
1868
+ * clip's end — the standard way to end a clip for a cut; `in` at position
1869
+ * 0 starts the clip covered and reveals it.
1870
+ * `transition.through(target, { via?, char?, word?, swap?, land?, color?,
1871
+ * duration?, outDuration? }, position?)` is the GO-THROUGH: dive INTO
1872
+ * something until its color takes over the frame, world-swap under the
1873
+ * cover, pull out into the new world. `via` picks the dive geometry:
1874
+ * 'surface' (default, the element's face), 'glyph' (`{ via: 'glyph',
1875
+ * char: 'o' }` — through the character's enclosed counter, measured from
1876
+ * the real glyph outline on plain text: no wrapper spans, no hand-tuned
1877
+ * margins), 'word' (`{ via: 'word', word: 'world' }` — the word's rect),
1878
+ * 'anchor' (match-cut: the subject persists across the cut — the same
1879
+ * selector's twin in the new world is the landing — while everything
1880
+ * around it changes). Never hand-roll a dive (a magic-scale camera.move +
1881
+ * a hand-timed swap): through() computes the exact cover scale, measures
1882
+ * at the dive's own start time, and guarantees full coverage at the cut.
1883
+ * All return `{ duration }` (full span including
1884
+ * stagger) for sequencing, and accept `(target, effect, opts?, position?)`
1885
+ * with opts `{ by?: 'chars'|'words'|'lines', duration?, stagger?, ease?,
1886
+ * from?: 'start'|'center'|'end'|'edges' }`. */
1557
1887
  text: TextKit
1558
- /** Named logo/brand-mark animations (all deterministic). Target the
1559
- * element wrapping the mark — an inline `<svg>`, an `<img>`, or a div
1560
- * around one. `logo.reveal(el, effect)` in: pop, blur, rise, wipe, iris,
1561
- * slit, spin, flip, bounce, glitch, draw, build. `draw` (strokes trace the
1562
- * SVG paths, fills crossfade in — works even on fills-only logos) and
1563
- * `build` (shapes assemble one by one) need INLINE SVG with path/shape
1564
- * elements; on an `<img>` or path-less SVG they fall back to `wipe` with a
1565
- * console warning. `logo.exit(el, effect)` out: pop, blur, fade, wipe,
1566
- * iris, drop. `logo.emphasize(el, 'pulse'|'breathe'|'wiggle')` is a
1567
- * settle-back gesture (breathe takes `repeats`, finite). `logo.shine(el)`
1568
- * sweeps a glint across the mark, masked by the logo's own silhouette.
1569
- * All return `{ duration }` (full span including stagger) and accept
1570
- * `(target, effect, opts?, position?)` with opts `{ duration?, ease?,
1571
- * stagger?, direction?: 'left'|'right'|'up'|'down' }`. */
1888
+ /** Named logo/brand-mark animations (all deterministic). Target the
1889
+ * element wrapping the mark — an inline `<svg>`, an `<img>`, or a div
1890
+ * around one. `logo.reveal(el, effect)` in: pop, blur, rise, wipe, iris,
1891
+ * slit, spin, flip, bounce, glitch, draw, build. `draw` (strokes trace the
1892
+ * SVG paths, fills crossfade in — works even on fills-only logos) and
1893
+ * `build` (shapes assemble one by one) need INLINE SVG with path/shape
1894
+ * elements; on an `<img>` or path-less SVG they fall back to `wipe` with a
1895
+ * console warning. `logo.exit(el, effect)` out: pop, blur, fade, wipe,
1896
+ * iris, drop. `logo.emphasize(el, 'pulse'|'breathe'|'wiggle')` is a
1897
+ * settle-back gesture (breathe takes `repeats`, finite). `logo.shine(el)`
1898
+ * sweeps a glint across the mark, masked by the logo's own silhouette.
1899
+ * All return `{ duration }` (full span including stagger) and accept
1900
+ * `(target, effect, opts?, position?)` with opts `{ duration?, ease?,
1901
+ * stagger?, direction?: 'left'|'right'|'up'|'down' }`. */
1572
1902
  logo: LogoKit
1573
1903
  transition: TransitionKit
1574
- /** Real 3D — a Three.js layer under the same paused timeline. Use for
1575
- * depth After Effects fakes and CSS perspective cannot reach: real
1576
- * cameras, lights, materials. `const L = three.layer({ camera: {z: 6} })`
1577
- * gives `{ scene, camera, canvas, drive }` — add meshes and lights to
1578
- * `L.scene` (import * as THREE from 'three'), animate positions/rotations
1579
- * with ordinary `tl.to(mesh.position, {...})` tweens, then
1580
- * `L.drive(duration, (t) => {...}, opts?, position?)` renders the layer
1581
- * over that span (set uniforms/camera from `t` ALONE — no clocks, no
1582
- * setAnimationLoop, no accumulation; the frame must be a pure function of
1583
- * timeline position). The canvas is a normal stage element: tween its
1584
- * opacity, put fx/mask on it, cut it with tl.set. */
1904
+ /** Real 3D — a Three.js layer under the same paused timeline. Use for
1905
+ * depth After Effects fakes and CSS perspective cannot reach: real
1906
+ * cameras, lights, materials. `const L = three.layer({ camera: {z: 6} })`
1907
+ * gives `{ scene, camera, canvas, drive }` — add meshes and lights to
1908
+ * `L.scene` (import * as THREE from 'three'), animate positions/rotations
1909
+ * with ordinary `tl.to(mesh.position, {...})` tweens, then
1910
+ * `L.drive(duration, (t) => {...}, opts?, position?)` renders the layer
1911
+ * over that span (set uniforms/camera from `t` ALONE — no clocks, no
1912
+ * setAnimationLoop, no accumulation; the frame must be a pure function of
1913
+ * timeline position). The canvas is a normal stage element: tween its
1914
+ * opacity, put fx/mask on it, cut it with tl.set. */
1585
1915
  three: ThreeKit
1586
- /** Variable-font axis animation — type whose WEIGHT is the thing that
1587
- * moves. After Effects cannot do this at all. `axis.animate(el, axis,
1588
- * effect, opts?, position?)`: axes `wght` (weight), `wdth` (width), `slnt`
1589
- * (slant), `opsz` (optical size), or any custom axis name; effects `ramp`
1590
- * (go and hold), `pulse` (one breath), `wave` (travels through the
1591
- * characters). Opts `{ from?, to?, duration?, ease?, stagger? }`. On a
1592
- * non-variable font it degrades honestly (wght -> weight staircase, wdth ->
1593
- * scaleX, slnt -> skewX) with a console warning, never a throw. Geist (the
1594
- * house family) IS variable — weight 100-900. */
1916
+ /** Variable-font axis animation — type whose WEIGHT is the thing that
1917
+ * moves. After Effects cannot do this at all. `axis.animate(el, axis,
1918
+ * effect, opts?, position?)`: axes `wght` (weight), `wdth` (width), `slnt`
1919
+ * (slant), `opsz` (optical size), or any custom axis name; effects `ramp`
1920
+ * (go and hold), `pulse` (one breath), `wave` (travels through the
1921
+ * characters). Opts `{ from?, to?, duration?, ease?, stagger? }`. On a
1922
+ * non-variable font it degrades honestly (wght -> weight staircase, wdth ->
1923
+ * scaleX, slnt -> skewX) with a console warning, never a throw. Geist (the
1924
+ * house family) IS variable — weight 100-900. */
1595
1925
  axis: AxisKit
1596
- /** The soundtrack's offline analysis (music/analysis/<track>.json).
1597
- * `audio.beat(n)` / `audio.downbeats` place cuts on the REAL grid — the
1598
- * downbeat phase is measured, not assumed to start at 0. `audio.onsets`
1599
- * are the strongest hits; land impact beats on them. `audio.drive(el, map)`
1600
- * makes a property breathe with the track. When no analysis exists,
1601
- * `audio.live` is false and every envelope reads 0 — never throws. */
1926
+ /** The soundtrack's offline analysis (music/analysis/<track>.json).
1927
+ * `audio.beat(n)` / `audio.downbeats` place cuts on the REAL grid — the
1928
+ * downbeat phase is measured, not assumed to start at 0. `audio.onsets`
1929
+ * are the strongest hits; land impact beats on them. `audio.drive(el, map)`
1930
+ * makes a property breathe with the track. When no analysis exists,
1931
+ * `audio.live` is false and every envelope reads 0 — never throws. */
1602
1932
  audio: AudioKit
1603
- /** Ask the exporter for TRUE temporal motion blur over an interval —
1604
- * sub-frame supersampling at render time, the thing a real-time tool
1605
- * cannot do. Put it on fast camera moves and whip cuts; the preview does
1606
- * not show it (it renders one sample per frame), the MP4 does.
1607
- * `ctx.blur(from, to)` marks [from, to] seconds. Opts: `shutter` degrees
1608
- * (0-360, default 180 — wider smears more), `tier` 'light' (4 samples) or
1609
- * 'full' (8, final renders). Costs render time roughly x samples over the
1610
- * marked intervals only, so mark moves, not holds. */
1933
+ /** Ask the exporter for TRUE temporal motion blur over an interval —
1934
+ * sub-frame supersampling at render time, the thing a real-time tool
1935
+ * cannot do. Put it on fast camera moves and whip cuts; the preview does
1936
+ * not show it (it renders one sample per frame), the MP4 does.
1937
+ * `ctx.blur(from, to)` marks [from, to] seconds. Opts: `shutter` degrees
1938
+ * (0-360, default 180 — wider smears more), `tier` 'light' (4 samples) or
1939
+ * 'full' (8, final renders). Costs render time roughly x samples over the
1940
+ * marked intervals only, so mark moves, not holds. */
1611
1941
  blur: (
1612
1942
  from: number,
1613
1943
  to: number,
1614
1944
  opts?: { shutter?: number; tier?: 'light' | 'full' }
1615
1945
  ) => void
1616
- /** Register async prep (texture decode, analysis fetch) started inside the
1617
- * direction. buildScene awaits every registered promise before setting
1618
- * window.__ready, so the exporter never screenshots a half-loaded scene.
1619
- * For kit authors — a direction rarely needs it directly. */
1946
+ /** Register async prep (texture decode, analysis fetch) started inside the
1947
+ * direction. buildScene awaits every registered promise before setting
1948
+ * window.__ready, so the exporter never screenshots a half-loaded scene.
1949
+ * For kit authors — a direction rarely needs it directly. */
1620
1950
  __await: (work: Promise<unknown>) => void
1621
- /** Visual effects — CSS filters and SVG filter primitives, all
1622
- * deterministic. Three verbs, all `(target, effect, opts?, position?)`:
1623
- * `fx.apply` ramps an effect on and leaves it, `fx.clear` ramps it off,
1624
- * and `fx.spike` does both in ONE gesture (one Tweak row, not two).
1625
- *
1626
- * Effects: `blur` (gaussian), `glow` (halo in the element's own colour),
1627
- * `saturate`, `dim` (brightness), `hue` (rotation), `smear` (DIRECTIONAL
1628
- * blur along `angle` — CSS blur cannot do this), `chroma` (chromatic
1629
- * aberration, the glitch look), `displace` (turbulence warp: heat haze,
1630
- * liquid dissolve), `grain` (film grain), `bloom` (bright-pass glow),
1631
- * `threshold` (posterize to N levels).
1632
- *
1633
- * Opts `{ amount?, detail?, duration?, ease?, angle?, color? }` — `amount`
1634
- * is the effect's peak, `detail` its secondary constant (turbulence
1635
- * frequency, bloom radius), `angle` in degrees for smear/chroma.
1636
- *
1637
- * The whip-pan cut is `fx.spike(stage, 'blur', {}, 'whip1')` alongside a
1638
- * fast `camera.move` — do NOT hand-write a pair of
1639
- * `tl.to(stage, { filter: 'blur(12px)' })` tweens, which produces two
1640
- * unsynced rows in the Tweak panel. */
1951
+ /** Visual effects — CSS filters and SVG filter primitives, all
1952
+ * deterministic. Three verbs, all `(target, effect, opts?, position?)`:
1953
+ * `fx.apply` ramps an effect on and leaves it, `fx.clear` ramps it off,
1954
+ * and `fx.spike` does both in ONE gesture (one Tweak row, not two).
1955
+ *
1956
+ * Effects: `blur` (gaussian), `glow` (halo in the element's own colour),
1957
+ * `saturate`, `dim` (brightness), `hue` (rotation), `smear` (DIRECTIONAL
1958
+ * blur along `angle` — CSS blur cannot do this), `chroma` (chromatic
1959
+ * aberration, the glitch look), `displace` (turbulence warp: heat haze,
1960
+ * liquid dissolve), `grain` (film grain), `bloom` (bright-pass glow),
1961
+ * `threshold` (posterize to N levels).
1962
+ *
1963
+ * Opts `{ amount?, detail?, duration?, ease?, angle?, color? }` — `amount`
1964
+ * is the effect's peak, `detail` its secondary constant (turbulence
1965
+ * frequency, bloom radius), `angle` in degrees for smear/chroma.
1966
+ *
1967
+ * The whip-pan cut is `fx.spike(stage, 'blur', {}, 'whip1')` alongside a
1968
+ * fast `camera.move` — do NOT hand-write a pair of
1969
+ * `tl.to(stage, { filter: 'blur(12px)' })` tweens, which produces two
1970
+ * unsynced rows in the Tweak panel. */
1641
1971
  fx: FilterKit
1642
- /** Interaction actions — the cursor is a STATEFUL ACTOR, not a per-call
1643
- * sprite. The engine tracks where every cursor is and whether it is on
1644
- * screen: continuation from the last resting position is the default, and
1645
- * entry/exit are explicit moments. First action enters from the nearest
1646
- * frame edge (`enter: 'fade'` materializes near the target instead); the
1647
- * next action continues from wherever the last one parked; `cursor.exit()`
1648
- * leaves via the nearest edge. `cursor.click(el)` travels, auto-morphs
1649
- * arrow→pointer on approach, and squashes cursor AND target (squash derived
1650
- * from target size). `cursor.drag(payload, to)` carries the payload locked
1651
- * to the cursor in one tween pair. `then: 'stay'|'exit'|'park'|selector`
1652
- * says where the cursor goes after an action. `cursor.actor(name, { icon,
1653
- * color, label })` declares independent named cursors (multi-user scenes —
1654
- * 'collab' icon carries a Figma-style name chip); two actors animate
1655
- * concurrently just by overlapping positions. Icons carry REGISTERED
1656
- * hotspots, so click math cannot drift. Every action defaults to
1657
- * `ease-in-out-cubic` and takes `duration`. */
1972
+ /** Interaction actions — the cursor is a STATEFUL ACTOR, not a per-call
1973
+ * sprite. The engine tracks where every cursor is and whether it is on
1974
+ * screen: continuation from the last resting position is the default, and
1975
+ * entry/exit are explicit moments. First action enters from the nearest
1976
+ * frame edge (`enter: 'fade'` materializes near the target instead); the
1977
+ * next action continues from wherever the last one parked; `cursor.exit()`
1978
+ * leaves via the nearest edge. `cursor.click(el)` travels, auto-morphs
1979
+ * arrow→pointer on approach, and squashes cursor AND target (squash derived
1980
+ * from target size). `cursor.drag(payload, to)` carries the payload locked
1981
+ * to the cursor in one tween pair. `then: 'stay'|'exit'|'park'|selector`
1982
+ * says where the cursor goes after an action. `cursor.actor(name, { icon,
1983
+ * color, label })` declares independent named cursors (multi-user scenes —
1984
+ * 'collab' icon carries a Figma-style name chip); two actors animate
1985
+ * concurrently just by overlapping positions. Icons carry REGISTERED
1986
+ * hotspots, so click math cannot drift. Every action defaults to
1987
+ * `ease-in-out-cubic` and takes `duration`. */
1658
1988
  cursor: CursorKit
1659
- /** Squash without a cursor — keyboard sends, programmatic actions. Amount
1660
- * defaults from the element's size (big surfaces squash less). */
1989
+ /** Squash without a cursor — keyboard sends, programmatic actions. Amount
1990
+ * defaults from the element's size (big surfaces squash less). */
1661
1991
  press: ActionsKit['press']
1662
- /** Animated scroll of a container until a target is in view (`duration: 0`
1663
- * sets it instantly for initial state). */
1992
+ /** Animated scroll of a container until a target is in view (`duration: 0`
1993
+ * sets it instantly for initial state). */
1664
1994
  scroll: ActionsKit['scroll']
1665
- /** Declarative hard UI state change on the timeline, seek-reversible:
1666
- * `ui.set(el, { text?, value?, src?, attrs?, classes?, flash? })`. Instant
1667
- * by design — a cut is hard; `flash` adds the one animated accent. */
1995
+ /** Declarative hard UI state change on the timeline, seek-reversible:
1996
+ * `ui.set(el, { text?, value?, src?, attrs?, classes?, flash? })`. Instant
1997
+ * by design — a cut is hard; `flash` adds the one animated accent. */
1668
1998
  ui: ActionsKit['ui']
1669
- /** Annotation primitives for pre-existing components — the "look here"
1670
- * vocabulary. `highlight.ring(el)` draws a rounded ring around the element
1671
- * (follows its corner radius); `circle` is the hand-drawn red-pen loop with
1672
- * overshoot; `marker` is a highlighter swipe behind the glyphs (multiply
1673
- * blend, sized from line height); `underline(el, 'sketch'|'straight'|
1674
- * 'marker')`; `glow` is one soft halo pulse; `spotlight(el)` dims
1675
- * everything EXCEPT the target behind a feathered cutout — the strongest
1676
- * "look here" available. All geometry derives from the target; every
1677
- * highlight takes `persist: 'hold' | seconds` and `release: 'fade' |
1678
- * 'undraw'`, so nothing loiters. Pairs with the cursor kit: cursor.click +
1679
- * highlight.ring is the product-tour grammar. */
1999
+ /** Annotation primitives for pre-existing components — the "look here"
2000
+ * vocabulary. `highlight.ring(el)` draws a rounded ring around the element
2001
+ * (follows its corner radius); `circle` is the hand-drawn red-pen loop with
2002
+ * overshoot; `marker` is a highlighter swipe behind the glyphs (multiply
2003
+ * blend, sized from line height); `underline(el, 'sketch'|'straight'|
2004
+ * 'marker')`; `glow` is one soft halo pulse; `spotlight(el)` dims
2005
+ * everything EXCEPT the target behind a feathered cutout — the strongest
2006
+ * "look here" available. All geometry derives from the target; every
2007
+ * highlight takes `persist: 'hold' | seconds` and `release: 'fade' |
2008
+ * 'undraw'`, so nothing loiters. Pairs with the cursor kit: cursor.click +
2009
+ * highlight.ring is the product-tour grammar. */
1680
2010
  highlight: HighlightKit
1681
- /** Scene lighting. `light.set({ angle, warmth })` declares THE light once —
1682
- * sweeps, rims, and cast shadows all derive their direction from it, so
1683
- * shadows can never disagree. `light.sweep(el)` runs a glint across any
1684
- * surface; `light.rim(el)` is the premium edge light. */
2011
+ /** Scene lighting. `light.set({ angle, warmth })` declares THE light once —
2012
+ * sweeps, rims, and cast shadows all derive their direction from it, so
2013
+ * shadows can never disagree. `light.sweep(el)` runs a glint across any
2014
+ * surface; `light.rim(el)` is the premium edge light. */
1685
2015
  light: LooksKit['light']
1686
- /** Shading and grade. `shade.cast(el)` — soft shadow offset along the
1687
- * declared scene light; `shade.vignette()` — one held frame vignette;
1688
- * `shade.grade('warm-product'|'cool-saas'|'noir')` — named color grade on
1689
- * the scene content (overlays stay ungraded and legible). */
2016
+ /** Shading and grade. `shade.cast(el)` — soft shadow offset along the
2017
+ * declared scene light; `shade.vignette()` — one held frame vignette;
2018
+ * `shade.grade('warm-product'|'cool-saas'|'noir')` — named color grade on
2019
+ * the scene content (overlays stay ungraded and legible). */
1690
2020
  shade: LooksKit['shade']
1691
- /** Ambient animated backdrop mounted BEHIND the scene root (the scene's
1692
- * own background must let it show through). Styles: 'gradient-drift',
1693
- * 'orbs', 'dot-grid'. Presets are tuned to stay alive but sub-salient
1694
- * (~0.5–2% pixel change per interval); `energy` scales within the budget.
1695
- * The returned handle's `calm()`/`lift()` duck the backdrop under dense
1696
- * foreground beats — audio ducking for pixels. */
2021
+ /** Ambient animated backdrop mounted BEHIND the scene root (the scene's
2022
+ * own background must let it show through). Styles: 'gradient-drift',
2023
+ * 'orbs', 'dot-grid'. Presets are tuned to stay alive but sub-salient
2024
+ * (~0.5–2% pixel change per interval); `energy` scales within the budget.
2025
+ * The returned handle's `calm()`/`lift()` duck the backdrop under dense
2026
+ * foreground beats — audio ducking for pixels. */
1697
2027
  backdrop: LooksKit['backdrop']
1698
- /** The angle-zoom product shot: the camera pushes toward one corner of the
1699
- * target (`anchor: 'bottom-right'` = the submit-button corner) WHILE the
1700
- * panel tilts a few degrees in perspective, a sheen sweeps the surface as
1701
- * the angle changes, and the shadow deepens. Full round trip by default
1702
- * (in → hold → settle); `settle: false` stays in the pose. Perspective is
1703
- * applied to the target only, never the stage. */
2028
+ /** The angle-zoom product shot: the camera pushes toward one corner of the
2029
+ * target (`anchor: 'bottom-right'` = the submit-button corner) WHILE the
2030
+ * panel tilts a few degrees in perspective, a sheen sweeps the surface as
2031
+ * the angle changes, and the shadow deepens. Full round trip by default
2032
+ * (in → hold → settle); `settle: false` stays in the pose. Perspective is
2033
+ * applied to the target only, never the stage. */
1704
2034
  hero: LooksKit['hero']
1705
- /** Standalone perspective pose for an element: `tilt(el, { pitch, yaw })`,
1706
- * tilt back to 0 to flatten. */
2035
+ /** Standalone perspective pose for an element: `tilt(el, { pitch, yaw })`,
2036
+ * tilt back to 0 to flatten. */
1707
2037
  tilt: LooksKit['tilt']
1708
- /** Camera FOLLOWS a moving element — per-frame re-centering at a locked
1709
- * occupancy, seek-deterministic (the catch-up is an eased blend from a
1710
- * declared start pose, not stateful smoothing). AUTHOR IT AFTER the motion
1711
- * it follows. `track(el, { duration, height, release: 'full'|'hold' })`. */
2038
+ /** Camera FOLLOWS a moving element — per-frame re-centering at a locked
2039
+ * occupancy, seek-deterministic (the catch-up is an eased blend from a
2040
+ * declared start pose, not stateful smoothing). AUTHOR IT AFTER the motion
2041
+ * it follows. `track(el, { duration, height, release: 'full'|'hold' })`. */
1712
2042
  track: LooksKit['track']
1713
- /** A sentence as a stateful object. `sentence(el)` takes over the element's
1714
- * text (word spans) and returns a handle: `.append(words)` — words land
1715
- * into the held line; `.swap(slot, [...])` — one word cycles in place;
1716
- * `.stress(word)` — the one-accent-color semantic emphasis; `.morphWord`
1717
- * ("it!" → "it?"); `.chip(word, media)` — a word slot holds a media chip.
1718
- * New words are mounted hidden at build (layout stays stable); `recenter`
1719
- * eases the container instead of live reflow. */
2043
+ /** A sentence as a stateful object. `sentence(el)` takes over the element's
2044
+ * text (word spans) and returns a handle: `.append(words)` — words land
2045
+ * into the held line; `.swap(slot, [...])` — one word cycles in place;
2046
+ * `.stress(word)` — the one-accent-color semantic emphasis; `.morphWord`
2047
+ * ("it!" → "it?"); `.chip(word, media)` — a word slot holds a media chip.
2048
+ * New words are mounted hidden at build (layout stays stable); `recenter`
2049
+ * eases the container instead of live reflow. */
1720
2050
  sentence: SentenceKit['sentence']
1721
- /** Universal shape morph: any element becomes any other element, a rect, or
1722
- * `'frame'`. Both real elements ride ONE interpolated box (position, size,
1723
- * corner radius, arc lift) while the source's chrome is driven onto the
1724
- * target's — at `handoff` the two are the same picture, so the surfaces
1725
- * HARD-CUT and never crossfade. Matched sub-components (`data-morph="name"`
1726
- * on both sides) travel; unmatched content displaces along the travel
1727
- * vector; children are counter-scaled so nothing stretches.
1728
- *
1729
- * Author the TARGET hidden (`autoAlpha: 0`); the morph reveals it at
1730
- * arrival. Every visual state is a pure function of eased progress, so it
1731
- * is export-worker safe. For MANY-to-many structure — recursing into
1732
- * sub-components, N→1 gathers, 1→N splits — install `kit/morphtree`. */
2051
+ /** Universal shape morph: any element becomes any other element, a rect, or
2052
+ * `'frame'`. Both real elements ride ONE interpolated box (position, size,
2053
+ * corner radius, arc lift) while the source's chrome is driven onto the
2054
+ * target's — at `handoff` the two are the same picture, so the surfaces
2055
+ * HARD-CUT and never crossfade. Matched sub-components (`data-morph="name"`
2056
+ * on both sides) travel; unmatched content displaces along the travel
2057
+ * vector; children are counter-scaled so nothing stretches.
2058
+ *
2059
+ * Author the TARGET hidden (`autoAlpha: 0`); the morph reveals it at
2060
+ * arrival. Every visual state is a pure function of eased progress, so it
2061
+ * is export-worker safe. For recursive structure, N→1 gathers and 1→N splits use `morphTree()`. */
1733
2062
  morph: MorphKit['morph']
1734
- /** The chip⇄full-bleed round trip: grows the element to cover the frame,
1735
- * holds, returns home (`back: false` to stay). Scale IS the transition —
1736
- * the object persists (Apple News+ grammar). */
2063
+ /** Continuous vector material + recursive DOM continuity, on the master timeline. */
2064
+ morphSurface: (from:string|Element,to:string|Element,opts:MorphSurfaceOpts,position?:gsap.Position)=>MorphSurfaceResult
2065
+ /** Recursive correspondence, clustering and seek-safe semantic handoff. */
2066
+ morphTree: (from: string | Element, to: string | Element | HTMLElement[], opts?: MorphTreeOpts, position?: gsap.Position) => MorphTreeResult
2067
+ /** The chip⇄full-bleed round trip: grows the element to cover the frame,
2068
+ * holds, returns home (`back: false` to stay). Scale IS the transition —
2069
+ * the object persists (Apple News+ grammar). */
1737
2070
  expand: MorphKit['expand']
1738
- /** Elements arrive from BEHIND THE CAMERA onto the layout: each starts
1739
- * oversized and offset radially (the whole UI plane blown up past the
1740
- * lens), soft and translucent, then flies into its exact layout slot,
1741
- * staggered. `dropIn('[data-panel] > *', { from: 3.2, stagger: 0.1 })`.
1742
- * The container should already be visible; its CHILDREN drop in. */
2071
+ /** Elements arrive from BEHIND THE CAMERA onto the layout: each starts
2072
+ * oversized and offset radially (the whole UI plane blown up past the
2073
+ * lens), soft and translucent, then flies into its exact layout slot,
2074
+ * staggered. `dropIn('[data-panel] > *', { from: 3.2, stagger: 0.1 })`.
2075
+ * The container should already be visible; its CHILDREN drop in. */
1743
2076
  dropIn: EnsembleKit['dropIn']
1744
- /** Elements fly in from scattered planar directions ('edges' | 'center' |
1745
- * 'above') and settle into their exact layout slots — the tile-wall /
1746
- * place-on-canvas grammar. Ends at exact identity. */
2077
+ /** Elements fly in from scattered planar directions ('edges' | 'center' |
2078
+ * 'above') and settle into their exact layout slots — the tile-wall /
2079
+ * place-on-canvas grammar. Ends at exact identity. */
1747
2080
  assemble: EnsembleKit['assemble']
1748
- /** Inverse of assemble: elements explode radially out of their layout and
1749
- * fade. Layout is left clean (hidden, transforms reset). */
2081
+ /** Inverse of assemble: elements explode radially out of their layout and
2082
+ * fade. Layout is left clean (hidden, transforms reset). */
1750
2083
  scatter: EnsembleKit['scatter']
1751
- /** SHOWPIECE — elements fly ballistically through the frame: tossed 'up'
1752
- * and falling back out, raining 'down', or 'burst'. Items EXIT. Use on a
1753
- * story payoff (something launched/generated/released), at most 1–2
1754
- * showpieces per film — on an ambient beat this is noise. */
2084
+ /** SHOWPIECE — elements fly ballistically through the frame: tossed 'up'
2085
+ * and falling back out, raining 'down', or 'burst'. Items EXIT. Use on a
2086
+ * story payoff (something launched/generated/released), at most 1–2
2087
+ * showpieces per film — on an ambient beat this is noise. */
1755
2088
  fountain: EnsembleKit['fountain']
1756
- /** SHOWPIECE — generated celebration burst (no authored elements):
1757
- * particles with deterministic ballistics, spin, flutter, fade.
1758
- * `confetti({ at: '[data-submit]' })`. ONE per film, ONLY on a genuine
1759
- * payoff beat (shipped / delivered / won). */
2089
+ /** SHOWPIECE — generated celebration burst (no authored elements):
2090
+ * particles with deterministic ballistics, spin, flutter, fade.
2091
+ * `confetti({ at: '[data-submit]' })`. ONE per film, ONLY on a genuine
2092
+ * payoff beat (shipped / delivered / won). */
1760
2093
  confetti: EnsembleKit['confetti']
1761
- /** Gallery belt: the container's row of items scrolls continuously through
1762
- * a clipped viewport (children duplicated once; the wrap is seamless).
1763
- * A showcase beat — logos, covers, results — never ambient filler. */
2094
+ /** Gallery belt: the container's row of items scrolls continuously through
2095
+ * a clipped viewport (children duplicated once; the wrap is seamless).
2096
+ * A showcase beat — logos, covers, results — never ambient filler. */
1764
2097
  river: EnsembleKit['river']
1765
- /** Bento reflow: tiles in one grid continuously trade space — spans change
1766
- * and every tile FLIPs to its measured new slot, the grid staying full
1767
- * while cells resize around each other. Ends on the authored layout. */
2098
+ /** Bento reflow: tiles in one grid continuously trade space — spans change
2099
+ * and every tile FLIPs to its measured new slot, the grid staying full
2100
+ * while cells resize around each other. Ends on the authored layout. */
1768
2101
  bento: EnsembleKit['bento']
1769
- /** Choreographed waiting — the beat AI products actually live in.
1770
- * `stream.answer(el, text)` arrives word by word (machine writing, no
1771
- * caret); `stream.skeleton(el)` shimmers placeholders then resolves into
1772
- * the real content; `stream.status(el, ['Analyzing', 'Searching 3 sources'])`
1773
- * names the work. All seek-deterministic. */
2102
+ /** Choreographed waiting — the beat AI products actually live in.
2103
+ * `stream.answer(el, text)` arrives word by word (machine writing, no
2104
+ * caret); `stream.skeleton(el)` shimmers placeholders then resolves into
2105
+ * the real content; `stream.status(el, ['Analyzing', 'Searching 3 sources'])`
2106
+ * names the work. All seek-deterministic. */
1774
2107
  stream: StreamKit
1775
- /** The impact move. `slam(el)` lands type/cards hard from a perspective
1776
- * turn; `slam(el, { direction: 'away' })` tips them back out of frame.
1777
- * DISTORTION BELONGS TO MOTION: 'in' ends at exact identity, 'away' ends
1778
- * invisible — an element must never come to rest sheared. Pair with
1779
- * `blur()` across the fast window so the speed reads as smear, not a pop. */
2108
+ /** The impact move. `slam(el)` lands type/cards hard from a perspective
2109
+ * turn; `slam(el, { direction: 'away' })` tips them back out of frame.
2110
+ * DISTORTION BELONGS TO MOTION: 'in' ends at exact identity, 'away' ends
2111
+ * invisible — an element must never come to rest sheared. Pair with
2112
+ * `blur()` across the fast window so the speed reads as smear, not a pop. */
1780
2113
  slam: ImpactKit['slam']
1781
- /** Masks, mattes, and blend composition — the things `clip-path` cannot do.
1782
- *
1783
- * `mask.reveal(el, shape, opts?, position?)` and `mask.hide(...)` animate
1784
- * any element through a moving mask. Shapes: `wipe` (hard directional
1785
- * edge), `feather` (SOFT directional edge — clip-path cannot do this),
1786
- * `iris` (hard circle), `radial` (soft circle), `slit` (letterbox opening),
1787
- * `barn` (panels part from centre), `blinds` (N bands), `shape` (your own
1788
- * clip-path via `opts.shape`). Opts `{ direction?: 'left'|'right'|'up'|
1789
- * 'down', softness?: 0-1, bands?, at?: {x,y}, duration?, ease? }`.
1790
- * Prefer `feather` over `wipe` for anything soft or premium — a hard edge
1791
- * on a full-frame reveal reads as cheap.
1792
- *
1793
- * `mask.matte(target, source, opts?, position?)` makes `source`'s
1794
- * silhouette the target's visibility — v1 snapshots the source ONCE, so
1795
- * animating the source will not move the matte; use `{ sweep: true }` to
1796
- * travel it across the target instead.
1797
- *
1798
- * `mask.fill(el, { gradient? | image?, drift? })` fills an element's GLYPHS
1799
- * with a live layer and drifts it (background-clip: text).
1800
- *
1801
- * `mask.blend(el, mode, opts?, position?)` composites a layer against its
1802
- * beat. Modes: screen, multiply, overlay, difference, exclusion,
1803
- * color-dodge, soft-light, luminosity, plus-lighter, normal. `screen` for
1804
- * light on dark, `multiply` for ink on cream, `luminosity` for duotone. */
2114
+ /** Masks, mattes, and blend composition — the things `clip-path` cannot do.
2115
+ *
2116
+ * `mask.reveal(el, shape, opts?, position?)` and `mask.hide(...)` animate
2117
+ * any element through a moving mask. Shapes: `wipe` (hard directional
2118
+ * edge), `feather` (SOFT directional edge — clip-path cannot do this),
2119
+ * `iris` (hard circle), `radial` (soft circle), `slit` (letterbox opening),
2120
+ * `barn` (panels part from centre), `blinds` (N bands), `shape` (your own
2121
+ * clip-path via `opts.shape`). Opts `{ direction?: 'left'|'right'|'up'|
2122
+ * 'down', softness?: 0-1, bands?, at?: {x,y}, duration?, ease? }`.
2123
+ * Prefer `feather` over `wipe` for anything soft or premium — a hard edge
2124
+ * on a full-frame reveal reads as cheap.
2125
+ *
2126
+ * `mask.matte(target, source, opts?, position?)` makes `source`'s
2127
+ * silhouette the target's visibility — v1 snapshots the source ONCE, so
2128
+ * animating the source will not move the matte; use `{ sweep: true }` to
2129
+ * travel it across the target instead.
2130
+ *
2131
+ * `mask.fill(el, { gradient? | image?, drift? })` fills an element's GLYPHS
2132
+ * with a live layer and drifts it (background-clip: text).
2133
+ *
2134
+ * `mask.blend(el, mode, opts?, position?)` composites a layer against its
2135
+ * beat. Modes: screen, multiply, overlay, difference, exclusion,
2136
+ * color-dodge, soft-light, luminosity, plus-lighter, normal. `screen` for
2137
+ * light on dark, `multiply` for ink on cream, `luminosity` for duotone. */
1805
2138
  mask: MaskKit
1806
2139
  }
1807
2140