@cosmictraveler002/anim-kit 1.3.0 → 1.5.0

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.
@@ -0,0 +1,350 @@
1
+ /**
2
+ * GL rail — the WebGL overlay rail: physics below, bent cards above.
3
+ *
4
+ * `dragRail` owns the motion (intent/pos ticker, tanh rubber-band, throw
5
+ * momentum, Lenis-safe wheel, auto axis); this effect adds the GPU layer the
6
+ * flat DOM can't draw: every card re-rendered onto **one fixed canvas** whose
7
+ * surfaces bend around an invisible cylinder, over a perspective grid floor.
8
+ * Why it is shaped this way (audit notes):
9
+ *
10
+ * - **The DOM stays the source of truth.** Cards keep owning layout, labels,
11
+ * hit areas and alt text; each frame reads their viewport rects and the
12
+ * shader re-creates the pixels *beneath* the labels. A label never drifts
13
+ * off its card, because the card **is** the rect the label sits in — only
14
+ * the picture curves.
15
+ * - **The bend lives in the vertex shader.** Each vertex takes its offset
16
+ * along the rail axis (`uOffset + local`), maps it onto a cylinder of
17
+ * `uRadius` (`a = o / R`, `x = R·sin a`, `z = −R·(1−cos a)`) and re-centres
18
+ * so the card's middle stays glued to its DOM position while its edges
19
+ * foreshorten onto the curve. One uniform per card, no per-vertex CPU work
20
+ * — and the rotation the cards need arrives free from the bend itself.
21
+ * - **One renderer for the whole rail.** A single alpha canvas inserted
22
+ * *under* the track (the track's transform creates a stacking context that
23
+ * paints above it): labels stay DOM, card backgrounds stay transparent, and
24
+ * the only pixels the canvas needs are the `<img>`s it hides — each one
25
+ * only after its texture has actually loaded.
26
+ * - **The axis follows the layout.** `axis: "auto"` hands the same
27
+ * `resolveRailAxis()` rule to the physics *and* the bend: row overflows →
28
+ * horizontal rail + bend around Y, only the column overflows (mobile
29
+ * stack) → vertical rail + bend around X. One init serves both.
30
+ * - **Silent no-op ladder.** Missing target → no-op. No
31
+ * `WebGLRenderingContext` (jsdom, SSR, WebGL-disabled browsers) → the flat
32
+ * `dragRail` rail alone, *before* any context is requested (a probe call
33
+ * would itself log). Renderer construction throws → flat rail. Texture 404
34
+ * → that one card's `<img>` stays visible. Under `prefers-reduced-motion`
35
+ * nothing mounts at all (the outer `guard` owns that decision). Nothing
36
+ * ever logs.
37
+ * - **Fidelity.** `destroy()` unwinds the physics, then the GPU: ticker,
38
+ * observers, geometry, materials, textures and the renderer are disposed,
39
+ * the canvas is removed and image opacities + the stage's inline position
40
+ * are restored.
41
+ *
42
+ * glRail("[data-rail]", { radius: 1200, grid: true })
43
+ *
44
+ * CSS: the stage (viewport) `position: relative; overflow: hidden` — the
45
+ * canvas covers it; the track `display: flex; width: max-content; gap: 1rem;
46
+ * cursor: grab; user-select: none; touch-action: pan-y` (flip to a column +
47
+ * `width: 100%; height: max-content` for the vertical layout). Cards
48
+ * `position: relative` with a **transparent background** — labels paint above
49
+ * the canvas for free, the effect hides only the media.
50
+ */
51
+ import * as THREE from "three";
52
+ import { gsap, initGSAP } from "../core/gsap.js";
53
+ import { guard } from "../core/guard.js";
54
+ import { one, toArray } from "../core/util.js";
55
+ import { dragRail, resolveRailAxis } from "../effects/drag-rail.js";
56
+ /* Card: the cylinder bend in the vertex shader, cover-crop + SDF corners in
57
+ the fragment — the pixel contract is webglMedia's, bent to the rail. */
58
+ const CARD_VERT = /* glsl */ `
59
+ varying vec2 vUv;
60
+ uniform vec2 uSize; // card size, px
61
+ uniform float uOffset; // centre offset from the stage centre along the rail axis, px
62
+ uniform float uRadius; // cylinder radius, px
63
+ uniform float uAxis; // 0 = rail runs x (bend around Y), 1 = rail runs y (bend around X)
64
+
65
+ void main() {
66
+ vUv = uv;
67
+ vec3 p = vec3(position.xy * uSize, 0.0);
68
+ float along = uAxis < 0.5 ? p.x : p.y;
69
+ float a = (uOffset + along) / uRadius;
70
+ // Re-centre on the card's own middle: the centre stays glued to the DOM
71
+ // rect while the edges foreshorten onto the cylinder.
72
+ float bent = uRadius * sin(a) - uRadius * sin(uOffset / uRadius) + uOffset;
73
+ if (uAxis < 0.5) p.x = bent; else p.y = bent;
74
+ p.z = -uRadius * (1.0 - cos(a));
75
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
76
+ }
77
+ `;
78
+ const CARD_FRAG = /* glsl */ `
79
+ precision highp float;
80
+ varying vec2 vUv;
81
+ uniform sampler2D uTex;
82
+ uniform vec2 uRes;
83
+ uniform vec2 uTexSize;
84
+ uniform float uCorner;
85
+
86
+ float sdRoundBox(vec2 p, vec2 b, float r) {
87
+ vec2 q = abs(p) - b + r;
88
+ return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
89
+ }
90
+
91
+ void main() {
92
+ vec2 px = (vUv - 0.5) * uRes;
93
+ float d = sdRoundBox(px, uRes * 0.5, uCorner);
94
+ float alpha = 1.0 - smoothstep(-1.0, 1.0, d); // 1px-ish AA on the corner
95
+ if (alpha < 0.003) discard;
96
+
97
+ // object-fit: cover — window the texture around its centre.
98
+ vec2 scale = uRes / max(uTexSize, vec2(1.0));
99
+ vec2 cover = (vUv - 0.5) * (scale / max(scale.x, scale.y)) + 0.5;
100
+ gl_FragColor = vec4(texture2D(uTex, cover).rgb, alpha);
101
+ }
102
+ `;
103
+ /* Floor: one plane lying at the bottom of the stage, lines in world space,
104
+ fading toward the horizon and the sides, drifting slightly with the rail. */
105
+ const GRID_VERT = /* glsl */ `
106
+ varying vec2 vWorld; // world xz under this fragment
107
+ void main() {
108
+ vec4 wp = modelMatrix * vec4(position, 1.0);
109
+ vWorld = wp.xz;
110
+ gl_Position = projectionMatrix * viewMatrix * wp;
111
+ }
112
+ `;
113
+ const GRID_FRAG = /* glsl */ `
114
+ precision highp float;
115
+ varying vec2 vWorld;
116
+ uniform vec2 uShift; // parallax drift with the rail position, px
117
+ uniform vec3 uColor;
118
+ uniform float uOpacity;
119
+ uniform float uSpacing;
120
+
121
+ void main() {
122
+ vec2 cell = (vWorld + uShift) / uSpacing;
123
+ vec2 grad = abs(fract(cell - 0.5) - 0.5) / max(fwidth(cell), 1e-5);
124
+ float line = 1.0 - min(min(grad.x, grad.y), 1.0);
125
+ float depth = smoothstep(-5200.0, -400.0, vWorld.y); // melt into the horizon
126
+ float side = 1.0 - smoothstep(1300.0, 3600.0, abs(vWorld.x));
127
+ gl_FragColor = vec4(uColor, line * uOpacity * depth * side);
128
+ }
129
+ `;
130
+ /** Floor geometry constants — near edge slightly in front of the cards, far
131
+ edge past the horizon; the stage bottom sits where the z=0 plane ends. */
132
+ const GRID_SPAN = 7200;
133
+ const GRID_DEPTH = 6000;
134
+ const GRID_NEAR_Z = 520;
135
+ export function glRail(target, options = {}) {
136
+ initGSAP();
137
+ const track = one(target);
138
+ if (!track)
139
+ return () => { };
140
+ const { viewport, card = ":scope > *", axis = "auto", radius = 1200, corner = 16, grid = true, dpr = 2, lerp, edge, throwScale, wheel, onTick, } = options;
141
+ return guard(options, () => {
142
+ const vp = one(viewport) ?? track.parentElement ?? track;
143
+ /* ---------------- physics first: the rail works with or without a GPU ---------------- */
144
+ let lastPos = 0;
145
+ const stopPhysics = dragRail(track, {
146
+ viewport: vp,
147
+ axis,
148
+ lerp,
149
+ edge,
150
+ throwScale,
151
+ wheel,
152
+ // The outer guard already decided about reduced motion for both of us.
153
+ force: true,
154
+ onTick: (pos, velocity) => {
155
+ lastPos = pos;
156
+ onTick?.(pos, velocity);
157
+ },
158
+ });
159
+ /* ---------------- availability ladder (silent, no probes) ---------------- */
160
+ if (typeof window === "undefined" || !("WebGLRenderingContext" in window))
161
+ return stopPhysics;
162
+ let renderer;
163
+ try {
164
+ renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
165
+ }
166
+ catch {
167
+ return stopPhysics; // context refused — the flat DOM rail is the fallback
168
+ }
169
+ /* ---------------- stage + camera ---------------- */
170
+ const prevPosition = vp.style.position;
171
+ if (!prevPosition && getComputedStyle(vp).position === "static") {
172
+ vp.style.position = "relative";
173
+ }
174
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, dpr));
175
+ const scene = new THREE.Scene();
176
+ const camera = new THREE.PerspectiveCamera(45, 1, 10, 14000);
177
+ // Pixel-space at z = 0: the z=0 plane exactly fills the stage.
178
+ const fitCamera = (w, h) => {
179
+ camera.aspect = w / h;
180
+ camera.position.set(0, 0, h / 2 / Math.tan((camera.fov * Math.PI) / 360));
181
+ camera.updateProjectionMatrix();
182
+ };
183
+ /* ---------------- grid floor ---------------- */
184
+ let gridMesh = null;
185
+ let gridShift = null;
186
+ if (grid) {
187
+ gridShift = new THREE.Vector2();
188
+ const material = new THREE.ShaderMaterial({
189
+ vertexShader: GRID_VERT,
190
+ fragmentShader: GRID_FRAG,
191
+ uniforms: {
192
+ uShift: { value: gridShift },
193
+ uColor: { value: new THREE.Color(0.66, 0.7, 0.78) },
194
+ uOpacity: { value: 0.24 },
195
+ uSpacing: { value: 150 },
196
+ },
197
+ transparent: true,
198
+ depthWrite: false,
199
+ depthTest: false,
200
+ });
201
+ gridMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
202
+ gridMesh.rotation.x = -Math.PI / 2;
203
+ gridMesh.renderOrder = -1; // floor first, cards over it
204
+ scene.add(gridMesh);
205
+ }
206
+ const geometry = new THREE.PlaneGeometry(1, 1, 24, 12);
207
+ const entries = [];
208
+ const loader = new THREE.TextureLoader();
209
+ let disposed = false;
210
+ for (const el of toArray(card, track)) {
211
+ const img = el.querySelector("img");
212
+ const src = img?.currentSrc || img?.src;
213
+ if (!img || !src)
214
+ continue; // label-only card — nothing to paint
215
+ const uniforms = {
216
+ uTex: { value: null },
217
+ uRes: { value: new THREE.Vector2(1, 1) },
218
+ uTexSize: { value: new THREE.Vector2(1, 1) },
219
+ uCorner: { value: corner },
220
+ uSize: { value: new THREE.Vector2(1, 1) },
221
+ uOffset: { value: 0 },
222
+ uRadius: { value: radius },
223
+ uAxis: { value: 0 },
224
+ };
225
+ const material = new THREE.ShaderMaterial({
226
+ vertexShader: CARD_VERT,
227
+ fragmentShader: CARD_FRAG,
228
+ uniforms,
229
+ transparent: true,
230
+ depthWrite: false,
231
+ depthTest: false,
232
+ });
233
+ // Positions come from the shader (uOffset/uSize), not the 1×1 geometry —
234
+ // the default bounding sphere would cull every card off-screen.
235
+ const mesh = new THREE.Mesh(geometry, material);
236
+ mesh.frustumCulled = false;
237
+ mesh.visible = false;
238
+ scene.add(mesh);
239
+ const prevOpacity = img.style.opacity;
240
+ entries.push({ el, img, mesh, material, prevOpacity });
241
+ loader.load(src, (texture) => {
242
+ if (disposed) {
243
+ texture.dispose();
244
+ return;
245
+ }
246
+ texture.minFilter = THREE.LinearFilter;
247
+ texture.magFilter = THREE.LinearFilter;
248
+ // No sRGB decode: the raw shader passes sampled values through, so
249
+ // the texture stays an ordinary RGBA8 upload (same contract as
250
+ // webglMedia — built-in colourSpace conversion only happens inside
251
+ // three's own material chunks).
252
+ uniforms.uTex.value = texture;
253
+ uniforms.uTexSize.value.set(texture.image?.width || 1, texture.image?.height || 1);
254
+ mesh.visible = true;
255
+ img.style.opacity = "0"; // the canvas owns these pixels from here
256
+ }, undefined, () => {
257
+ /* 404 / decode failure — this card's <img> was never hidden. */
258
+ });
259
+ }
260
+ if (!entries.length) {
261
+ // Nothing renderable — plain dragRail is the whole effect.
262
+ geometry.dispose();
263
+ gridMesh?.geometry.dispose();
264
+ gridMesh?.material?.dispose();
265
+ renderer.dispose();
266
+ return stopPhysics;
267
+ }
268
+ /* ---------------- canvas: under the track, over the stage ---------------- */
269
+ const canvas = renderer.domElement;
270
+ canvas.style.cssText =
271
+ "position:absolute;inset:0;width:100%;height:100%;display:block;pointer-events:none;";
272
+ vp.insertBefore(canvas, track);
273
+ vp.dataset.akGlRail = "true";
274
+ /* ---------------- sizing ---------------- */
275
+ const resize = () => {
276
+ const w = Math.max(1, vp.clientWidth);
277
+ const h = Math.max(1, vp.clientHeight);
278
+ renderer.setSize(w, h, false);
279
+ fitCamera(w, h);
280
+ if (gridMesh) {
281
+ gridMesh.scale.set(GRID_SPAN, GRID_DEPTH, 1);
282
+ // Local +y maps to world −z, so centre the plane to span
283
+ // [GRID_NEAR_Z − GRID_DEPTH, GRID_NEAR_Z] in z.
284
+ gridMesh.position.set(0, -h / 2 + 2, GRID_NEAR_Z - GRID_DEPTH / 2);
285
+ }
286
+ };
287
+ resize();
288
+ let ro = null;
289
+ if (typeof ResizeObserver === "function") {
290
+ ro = new ResizeObserver(resize);
291
+ ro.observe(vp);
292
+ }
293
+ /* ---------------- loop ---------------- */
294
+ let visible = true;
295
+ let io = null;
296
+ if (typeof IntersectionObserver === "function") {
297
+ io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting), {
298
+ rootMargin: "120px",
299
+ });
300
+ io.observe(vp);
301
+ }
302
+ const sync = () => {
303
+ const stage = vp.getBoundingClientRect();
304
+ const cx = stage.left + stage.width / 2;
305
+ const cy = stage.top + stage.height / 2;
306
+ const vertical = resolveRailAxis(track, vp, axis) === "y";
307
+ for (const entry of entries) {
308
+ const rect = entry.el.getBoundingClientRect();
309
+ const u = entry.material.uniforms;
310
+ u.uOffset.value = vertical
311
+ ? cy - (rect.top + rect.height / 2)
312
+ : rect.left + rect.width / 2 - cx;
313
+ u.uSize.value.set(rect.width, rect.height);
314
+ u.uRes.value.set(rect.width, rect.height);
315
+ u.uAxis.value = vertical ? 1 : 0;
316
+ }
317
+ };
318
+ const render = () => {
319
+ if (!visible)
320
+ return;
321
+ sync();
322
+ gridShift?.set(lastPos * 0.1, 0); // subtle parallax: the floor trails the rail
323
+ renderer.render(scene, camera);
324
+ };
325
+ gsap.ticker.add(render);
326
+ /* ---------------- teardown ---------------- */
327
+ return () => {
328
+ disposed = true;
329
+ stopPhysics();
330
+ gsap.ticker.remove(render);
331
+ io?.disconnect();
332
+ ro?.disconnect();
333
+ for (const entry of entries) {
334
+ entry.material.uniforms.uTex.value?.dispose();
335
+ entry.material.dispose();
336
+ entry.img.style.opacity = entry.prevOpacity;
337
+ }
338
+ geometry.dispose();
339
+ if (gridMesh) {
340
+ gridMesh.geometry.dispose();
341
+ gridMesh.material.dispose();
342
+ }
343
+ renderer.dispose();
344
+ canvas.remove();
345
+ delete vp.dataset.akGlRail;
346
+ vp.style.position = prevPosition;
347
+ };
348
+ });
349
+ }
350
+ //# sourceMappingURL=gl-rail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gl-rail.js","sourceRoot":"","sources":["../../src/three/gl-rail.ts"],"names":[],"mappings":"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"}
@@ -6,8 +6,10 @@
6
6
  * The core barrel (`@cosmictraveler002/anim-kit`) never imports three, so
7
7
  * plain DOM/scroll users keep a gsap+lenis-only dependency tree.
8
8
  *
9
- * import { webglMedia } from "@cosmictraveler002/anim-kit/three";
9
+ * import { webglMedia, glRail } from "@cosmictraveler002/anim-kit/three";
10
10
  */
11
11
  export { webglMedia } from "./webgl-media.js";
12
12
  export type { WebglMediaOptions } from "./webgl-media.js";
13
+ export { glRail } from "./gl-rail.js";
14
+ export type { GlRailOptions } from "./gl-rail.js";
13
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/three/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/three/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
@@ -6,7 +6,8 @@
6
6
  * The core barrel (`@cosmictraveler002/anim-kit`) never imports three, so
7
7
  * plain DOM/scroll users keep a gsap+lenis-only dependency tree.
8
8
  *
9
- * import { webglMedia } from "@cosmictraveler002/anim-kit/three";
9
+ * import { webglMedia, glRail } from "@cosmictraveler002/anim-kit/three";
10
10
  */
11
11
  export { webglMedia } from "./webgl-media.js";
12
+ export { glRail } from "./gl-rail.js";
12
13
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/three/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/three/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmictraveler002/anim-kit",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Modular, framework-agnostic animation library — GSAP + ScrollTrigger + Lenis effects packaged as composable ES modules.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -4,10 +4,14 @@
4
4
  * Uses the canonical Lenis + GSAP recipe:
5
5
  * lenis.on("scroll", ScrollTrigger.update)
6
6
  * gsap.ticker.add(t => lenis.raf(t * 1000))
7
- * gsap.ticker.lagSmoothing(0)
8
7
  *
9
8
  * Driving Lenis from GSAP's ticker (rather than its own rAF) keeps both
10
9
  * clocks on the same frame, which is what stops ScrollTrigger from stuttering.
10
+ * Those ticker callbacks receive the *same* adjusted clock the tweens run on,
11
+ * so GSAP's lag smoothing is deliberately left on: a stalled frame (GC pause,
12
+ * buffer tick) advances one short step everywhere instead of snapping the
13
+ * scroll and every scroll-linked tween to a new time at once. See
14
+ * SmoothScrollOptions.lagSmoothing to retune it or restore the old opt-out.
11
15
  *
12
16
  * `useScrollerProxy: true` additionally proxies `document.documentElement`,
13
17
  * matching kalakritico.in's setup — only needed if you scroll a nested
@@ -31,6 +35,27 @@ export interface SmoothScrollOptions extends CommonOptions {
31
35
  initialScroll?: number;
32
36
  /** Proxy documentElement through ScrollTrigger (nested scrollers). @default false */
33
37
  useScrollerProxy?: boolean;
38
+ /** Touch-drag sensitivity — forwarded to Lenis `touchMultiplier`. @default 1 */
39
+ touchMultiplier?: number;
40
+ /** Mouse-wheel sensitivity — forwarded to Lenis `wheelMultiplier`. @default 1 */
41
+ wheelMultiplier?: number;
42
+ /**
43
+ * GSAP lag smoothing on the shared ticker: a frame longer than `threshold`
44
+ * advances only `adjustedLag` ms, so a hitch can't teleport the scroll and
45
+ * every scroll-linked tween in one jump.
46
+ *
47
+ * - `undefined` — leave GSAP's setup alone; its built-in compensation
48
+ * (500ms → 33ms) stays active.
49
+ * - `false` — disable lag smoothing (anim-kit ≤1.4 behaviour, what the
50
+ * Lenis README recipe asks for): timing stays exactly wall-clock, but
51
+ * every frame longer than the threshold snaps.
52
+ * - `{ threshold, adjustedLag }` — retune it. Lower the threshold (e.g.
53
+ * `150`) to smooth short mobile hitches; raise it if slow devices should
54
+ * keep real-time speed instead of briefly running slow-motion.
55
+ *
56
+ * `destroy()` restores GSAP's default (500/33) whenever this was set.
57
+ */
58
+ lagSmoothing?: false | { threshold?: number; adjustedLag?: number };
34
59
  }
35
60
 
36
61
  export interface SmoothScrollHandle {
@@ -61,6 +86,9 @@ export function smoothScroll(options: SmoothScrollOptions = {}): SmoothScrollHan
61
86
  orientation = "vertical",
62
87
  initialScroll = 0,
63
88
  useScrollerProxy = false,
89
+ touchMultiplier,
90
+ wheelMultiplier,
91
+ lagSmoothing,
64
92
  } = options;
65
93
 
66
94
  if (typeof window === "undefined") return INERT;
@@ -74,6 +102,9 @@ export function smoothScroll(options: SmoothScrollOptions = {}): SmoothScrollHan
74
102
  syncTouch: smoothTouch,
75
103
  orientation,
76
104
  autoRaf: false,
105
+ // undefined falls through to Lenis's own defaults (1 / 1).
106
+ touchMultiplier,
107
+ wheelMultiplier,
77
108
  });
78
109
 
79
110
  if (initialScroll) lenis.scrollTo(initialScroll, { immediate: true });
@@ -85,7 +116,17 @@ export function smoothScroll(options: SmoothScrollOptions = {}): SmoothScrollHan
85
116
  // 2. Run Lenis off GSAP's ticker so both share one frame clock.
86
117
  const tick = (time: number) => lenis.raf(time * 1000);
87
118
  gsap.ticker.add(tick);
88
- gsap.ticker.lagSmoothing(0);
119
+
120
+ // Lag smoothing drives *one* clock — the tweens' and this ticker's alike —
121
+ // so leaving GSAP's compensation on turns a stalled frame into one short
122
+ // step everywhere instead of a snap. Only touch the ticker when asked, so a
123
+ // host page's own setup is never clobbered.
124
+ let lagTouched = false;
125
+ if (lagSmoothing !== undefined) {
126
+ lagTouched = true;
127
+ if (lagSmoothing === false) gsap.ticker.lagSmoothing(0);
128
+ else gsap.ticker.lagSmoothing(lagSmoothing.threshold ?? 500, lagSmoothing.adjustedLag ?? 33);
129
+ }
89
130
 
90
131
  // 3. Optional: proxy the scroller (for nested/element scrollers).
91
132
  if (useScrollerProxy) {
@@ -126,6 +167,7 @@ export function smoothScroll(options: SmoothScrollOptions = {}): SmoothScrollHan
126
167
  window.removeEventListener("resize", onResize);
127
168
  window.removeEventListener("load", refresh);
128
169
  gsap.ticker.remove(tick);
170
+ if (lagTouched) gsap.ticker.lagSmoothing(500, 33); // undo our change — GSAP's default
129
171
  lenis.off("scroll", onScroll);
130
172
  if (useScrollerProxy) {
131
173
  ScrollTrigger.scrollerProxy(document.documentElement, {} as never);