use-scroller 1.2.0 → 2.0.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.
Files changed (42) hide show
  1. package/README.md +387 -95
  2. package/dist/chunk-BDIJT5M4.js +1188 -0
  3. package/dist/core.d.ts +121 -0
  4. package/dist/core.js +36 -0
  5. package/dist/index.d.ts +51 -0
  6. package/dist/index.js +346 -0
  7. package/package.json +38 -24
  8. package/dist/cjs/hooks/use-iso-morphic-effect.d.ts +0 -2
  9. package/dist/cjs/hooks/use-iso-morphic-effect.js +0 -6
  10. package/dist/cjs/hooks/use-scroll.d.ts +0 -24
  11. package/dist/cjs/hooks/use-scroll.js +0 -80
  12. package/dist/cjs/hooks/use-window-scroll.d.ts +0 -13
  13. package/dist/cjs/hooks/use-window-scroll.js +0 -62
  14. package/dist/cjs/index.d.ts +0 -3
  15. package/dist/cjs/index.js +0 -19
  16. package/dist/cjs/scroller.d.ts +0 -10
  17. package/dist/cjs/scroller.js +0 -94
  18. package/dist/cjs/types.d.ts +0 -17
  19. package/dist/cjs/types.js +0 -2
  20. package/dist/cjs/utils.d.ts +0 -37
  21. package/dist/cjs/utils.js +0 -92
  22. package/dist/esm/hooks/use-iso-morphic-effect.d.ts +0 -2
  23. package/dist/esm/hooks/use-iso-morphic-effect.js +0 -3
  24. package/dist/esm/hooks/use-scroll.d.ts +0 -24
  25. package/dist/esm/hooks/use-scroll.js +0 -76
  26. package/dist/esm/hooks/use-window-scroll.d.ts +0 -13
  27. package/dist/esm/hooks/use-window-scroll.js +0 -58
  28. package/dist/esm/index.d.ts +0 -3
  29. package/dist/esm/index.js +0 -3
  30. package/dist/esm/scroller.d.ts +0 -10
  31. package/dist/esm/scroller.js +0 -90
  32. package/dist/esm/types.d.ts +0 -17
  33. package/dist/esm/types.js +0 -1
  34. package/dist/esm/utils.d.ts +0 -37
  35. package/dist/esm/utils.js +0 -85
  36. package/src/hooks/use-iso-morphic-effect.ts +0 -4
  37. package/src/hooks/use-scroll.ts +0 -84
  38. package/src/hooks/use-window-scroll.ts +0 -73
  39. package/src/index.ts +0 -3
  40. package/src/scroller.ts +0 -107
  41. package/src/types.ts +0 -21
  42. package/src/utils.ts +0 -103
@@ -0,0 +1,1188 @@
1
+ // src/core/types.ts
2
+ var EDGE_TOLERANCE = 1;
3
+
4
+ // src/core/geometry.ts
5
+ var SCROLLABLE_OVERFLOW = ["auto", "scroll", "overlay"];
6
+ function isWindow(target) {
7
+ return "scrollY" in target;
8
+ }
9
+ function readScroll(target) {
10
+ if (isWindow(target)) return readWindowScroll(target);
11
+ return readElementScroll(target);
12
+ }
13
+ function readWindowScroll(win) {
14
+ const root = win.document.documentElement;
15
+ const viewportWidth = root.clientWidth;
16
+ const viewportHeight = hasClassicScrollbar(win) ? root.clientHeight : win.innerHeight;
17
+ return {
18
+ x: win.scrollX,
19
+ y: win.scrollY,
20
+ ...horizontalRange(root, Math.max(0, root.scrollWidth - viewportWidth)),
21
+ maxY: Math.max(0, root.scrollHeight - viewportHeight),
22
+ viewportWidth,
23
+ viewportHeight
24
+ };
25
+ }
26
+ function hasClassicScrollbar(win) {
27
+ return win.innerWidth > win.document.documentElement.clientWidth;
28
+ }
29
+ function readElementScroll(element) {
30
+ return {
31
+ x: element.scrollLeft,
32
+ y: element.scrollTop,
33
+ ...horizontalRange(element, element.scrollWidth - element.clientWidth),
34
+ maxY: element.scrollHeight - element.clientHeight,
35
+ viewportWidth: element.clientWidth,
36
+ viewportHeight: element.clientHeight
37
+ };
38
+ }
39
+ function horizontalRange(element, max) {
40
+ if (getComputedStyle(element).direction === "rtl") return { minX: -max, maxX: 0 };
41
+ return { minX: 0, maxX: max };
42
+ }
43
+ function writeScroll(target, position) {
44
+ target.scrollTo({ left: position.x, top: position.y, behavior: "instant" });
45
+ }
46
+ function isScrollable(element, axis) {
47
+ const style = getComputedStyle(element);
48
+ const overflow = axis === "x" ? style.overflowX : style.overflowY;
49
+ if (!SCROLLABLE_OVERFLOW.includes(overflow)) return false;
50
+ const overflowAmount = axis === "x" ? element.scrollWidth - element.clientWidth : element.scrollHeight - element.clientHeight;
51
+ return overflowAmount > EDGE_TOLERANCE;
52
+ }
53
+ function findScrollParent(element, axis) {
54
+ const root = element.ownerDocument.documentElement;
55
+ for (let el = element.parentElement; el && el !== root; el = el.parentElement) {
56
+ if (isScrollable(el, axis)) return el;
57
+ }
58
+ return window;
59
+ }
60
+
61
+ // src/core/easings.ts
62
+ var easings = {
63
+ linear: (t) => t,
64
+ easeInCubic: (t) => t * t * t,
65
+ easeOutCubic: (t) => 1 - (1 - t) ** 3,
66
+ easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2
67
+ };
68
+ var PRECISION = 1e-7;
69
+ var NEWTON_ITERATIONS = 8;
70
+ var MIN_SLOPE = 1e-6;
71
+ function cubicBezier(x1, y1, x2, y2) {
72
+ if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) {
73
+ throw new RangeError(`cubicBezier: x1 and x2 must be within [0, 1], got ${x1} and ${x2}`);
74
+ }
75
+ const xCurve = polynomial(x1, x2);
76
+ const yCurve = polynomial(y1, y2);
77
+ return (t) => {
78
+ if (t <= 0) return 0;
79
+ if (t >= 1) return 1;
80
+ return sample(yCurve, solveForParameter(xCurve, t));
81
+ };
82
+ }
83
+ function polynomial(p1, p2) {
84
+ const c = 3 * p1;
85
+ const b = 3 * (p2 - p1) - c;
86
+ return { a: 1 - c - b, b, c };
87
+ }
88
+ function sample({ a, b, c }, t) {
89
+ return ((a * t + b) * t + c) * t;
90
+ }
91
+ function slope({ a, b, c }, t) {
92
+ return (3 * a * t + 2 * b) * t + c;
93
+ }
94
+ function solveForParameter(curve, x) {
95
+ var _a;
96
+ return (_a = newton(curve, x)) != null ? _a : bisect(curve, x);
97
+ }
98
+ function newton(curve, x) {
99
+ let t = x;
100
+ for (let i = 0; i < NEWTON_ITERATIONS; i++) {
101
+ const error = sample(curve, t) - x;
102
+ if (Math.abs(error) < PRECISION) return t;
103
+ const derivative = slope(curve, t);
104
+ if (Math.abs(derivative) < MIN_SLOPE) return null;
105
+ t -= error / derivative;
106
+ if (t < 0 || t > 1) return null;
107
+ }
108
+ return null;
109
+ }
110
+ function bisect(curve, x) {
111
+ let low = 0;
112
+ let high = 1;
113
+ let t = x;
114
+ while (high - low > PRECISION) {
115
+ const value = sample(curve, t);
116
+ if (Math.abs(value - x) < PRECISION) return t;
117
+ if (value < x) low = t;
118
+ else high = t;
119
+ t = (low + high) / 2;
120
+ }
121
+ return t;
122
+ }
123
+
124
+ // src/core/inline-style.ts
125
+ function setInlineStyles(element, styles) {
126
+ const previous = Object.keys(styles).map((property) => ({
127
+ property,
128
+ value: element.style.getPropertyValue(property),
129
+ priority: element.style.getPropertyPriority(property)
130
+ }));
131
+ for (const [property, value] of Object.entries(styles)) element.style.setProperty(property, value);
132
+ return () => {
133
+ for (const { property, value, priority } of previous) {
134
+ if (value) element.style.setProperty(property, value, priority);
135
+ else element.style.removeProperty(property);
136
+ }
137
+ };
138
+ }
139
+
140
+ // src/core/animate-scroll.ts
141
+ var AXES = ["x", "y"];
142
+ var INTERRUPT_EVENTS = ["wheel", "touchstart", "pointerdown", "keydown"];
143
+ var SPRING_STEP_MS = 1e3 / 120;
144
+ var SETTLE_DISTANCE = 0.5;
145
+ var SETTLE_SPEED = 10;
146
+ var running = /* @__PURE__ */ new WeakMap();
147
+ function animateScroll(target, to, options = {}) {
148
+ const {
149
+ animation = { type: "tween" },
150
+ interruptible = true,
151
+ respectReducedMotion = true
152
+ } = options;
153
+ cancelScroll(target);
154
+ if (animation.type === "instant" || respectReducedMotion && prefersReducedMotion()) {
155
+ return jump(target, to);
156
+ }
157
+ const createMotion = animation.type === "tween" ? tween(animation) : spring(animation);
158
+ return run(target, to, createMotion, interruptible);
159
+ }
160
+ function cancelScroll(target) {
161
+ var _a;
162
+ (_a = running.get(target)) == null ? void 0 : _a.cancel();
163
+ }
164
+ function scrollDestination(target) {
165
+ const metrics = readScroll(target);
166
+ const current = { x: metrics.x, y: metrics.y };
167
+ const active = running.get(target);
168
+ return active ? resolveGoal(active.to, current, metrics) : current;
169
+ }
170
+ function clampToRange(metrics, axis, value) {
171
+ const min = axis === "x" ? metrics.minX : 0;
172
+ const max = axis === "x" ? metrics.maxX : metrics.maxY;
173
+ return Math.min(max, Math.max(min, value));
174
+ }
175
+ function resolveGoal(to, fallback, metrics) {
176
+ return {
177
+ x: to.x === void 0 ? fallback.x : clampToRange(metrics, "x", to.x),
178
+ y: to.y === void 0 ? fallback.y : clampToRange(metrics, "y", to.y)
179
+ };
180
+ }
181
+ function requestedAxes(to, position) {
182
+ return {
183
+ x: to.x === void 0 ? void 0 : position.x,
184
+ y: to.y === void 0 ? void 0 : position.y
185
+ };
186
+ }
187
+ function prefersReducedMotion() {
188
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
189
+ }
190
+ function jump(target, to) {
191
+ const metrics = readScroll(target);
192
+ writeScroll(target, requestedAxes(to, resolveGoal(to, metrics, metrics)));
193
+ return {
194
+ finished: Promise.resolve("completed"),
195
+ cancel() {
196
+ }
197
+ };
198
+ }
199
+ function run(target, to, createMotion, interruptible) {
200
+ const snapElement = isWindow(target) ? target.document.documentElement : target;
201
+ const restoreSnap = suspendSnap(snapElement);
202
+ const eventTarget = target;
203
+ const start = readScroll(target);
204
+ const from = { x: start.x, y: start.y };
205
+ const motion = createMotion(from);
206
+ const startedAt = performance.now();
207
+ let frame = requestAnimationFrame(step);
208
+ let stopped = false;
209
+ let resolveFinished;
210
+ const finished = new Promise((resolve) => resolveFinished = resolve);
211
+ const entry = { to, cancel: () => stop("cancelled") };
212
+ const interrupt = () => stop("interrupted");
213
+ function step(now) {
214
+ const metrics = readScroll(target);
215
+ const { position, done } = motion(Math.max(0, now - startedAt), resolveGoal(to, from, metrics));
216
+ writeScroll(target, requestedAxes(to, position));
217
+ if (done) stop("completed");
218
+ else frame = requestAnimationFrame(step);
219
+ }
220
+ function stop(result) {
221
+ if (stopped) return;
222
+ stopped = true;
223
+ cancelAnimationFrame(frame);
224
+ for (const type of INTERRUPT_EVENTS) eventTarget.removeEventListener(type, interrupt);
225
+ restoreSnap == null ? void 0 : restoreSnap();
226
+ if (running.get(target) === entry) running.delete(target);
227
+ resolveFinished(result);
228
+ }
229
+ if (interruptible) {
230
+ for (const type of INTERRUPT_EVENTS) {
231
+ eventTarget.addEventListener(type, interrupt, { passive: true });
232
+ }
233
+ }
234
+ running.set(target, entry);
235
+ return { finished, cancel: entry.cancel };
236
+ }
237
+ function suspendSnap(element) {
238
+ if (getComputedStyle(element).scrollSnapType === "none") return null;
239
+ return setInlineStyles(element, { "scroll-snap-type": "none" });
240
+ }
241
+ function tween({ duration = 450, easing = easings.easeOutCubic }) {
242
+ if (!(duration >= 0)) {
243
+ throw new RangeError(`animateScroll: tween duration must be >= 0, got ${duration}`);
244
+ }
245
+ return (from) => (elapsed, goal) => {
246
+ const progress2 = duration > 0 ? Math.min(1, elapsed / duration) : 1;
247
+ if (progress2 === 1) return { position: goal, done: true };
248
+ const eased = easing(progress2);
249
+ return {
250
+ position: { x: from.x + (goal.x - from.x) * eased, y: from.y + (goal.y - from.y) * eased },
251
+ done: false
252
+ };
253
+ };
254
+ }
255
+ function spring({
256
+ stiffness = 170,
257
+ damping = 26,
258
+ mass = 1,
259
+ velocity: initial = {}
260
+ }) {
261
+ for (const [name, value] of Object.entries({ stiffness, damping, mass })) {
262
+ if (!(value > 0)) {
263
+ throw new RangeError(`animateScroll: spring ${name} must be > 0, got ${value}`);
264
+ }
265
+ }
266
+ return (from) => {
267
+ var _a, _b;
268
+ const position = { ...from };
269
+ const velocity = { x: (_a = initial.x) != null ? _a : 0, y: (_b = initial.y) != null ? _b : 0 };
270
+ const dt = SPRING_STEP_MS / 1e3;
271
+ let stepsTaken = 0;
272
+ return (elapsed, goal) => {
273
+ for (; stepsTaken < Math.floor(elapsed / SPRING_STEP_MS); stepsTaken++) {
274
+ for (const axis of AXES) {
275
+ const force = -stiffness * (position[axis] - goal[axis]) - damping * velocity[axis];
276
+ velocity[axis] += force / mass * dt;
277
+ position[axis] += velocity[axis] * dt;
278
+ }
279
+ }
280
+ const settled = AXES.every(
281
+ (axis) => Math.abs(position[axis] - goal[axis]) < SETTLE_DISTANCE && Math.abs(velocity[axis]) < SETTLE_SPEED
282
+ );
283
+ return settled ? { position: goal, done: true } : { position: { ...position }, done: false };
284
+ };
285
+ };
286
+ }
287
+
288
+ // src/core/observe-scroll.ts
289
+ var IDLE_MOTION = {
290
+ directionX: 0,
291
+ directionY: 0,
292
+ velocityX: 0,
293
+ velocityY: 0,
294
+ isScrolling: false
295
+ };
296
+ var initialScrollState = {
297
+ x: 0,
298
+ y: 0,
299
+ minX: 0,
300
+ maxX: 0,
301
+ maxY: 0,
302
+ progressX: 0,
303
+ progressY: 0,
304
+ atTop: true,
305
+ atBottom: true,
306
+ atLeft: true,
307
+ atRight: true,
308
+ canScrollX: false,
309
+ canScrollY: false,
310
+ ...IDLE_MOTION
311
+ };
312
+ var MAX_TIMEOUT_MS = 2147483647;
313
+ var STATE_KEYS = Object.keys(initialScrollState);
314
+ function observeScroll(target, listener, { idleDelay = 120 } = {}) {
315
+ if (!(idleDelay >= 0 && idleDelay <= MAX_TIMEOUT_MS)) {
316
+ throw new RangeError(
317
+ `observeScroll: idleDelay must be between 0 and ${MAX_TIMEOUT_MS} ms, got ${idleDelay}`
318
+ );
319
+ }
320
+ let state = { ...describePosition(readScroll(target)), ...IDLE_MOTION };
321
+ let sampledAt = performance.now();
322
+ let scrolling = false;
323
+ let scrolledSinceUpdate = false;
324
+ let frame = null;
325
+ let idleTimer;
326
+ function update() {
327
+ const now = performance.now();
328
+ const metrics = readScroll(target);
329
+ const next = nextState(state, metrics, scrolling, currentVelocity(metrics, now - sampledAt));
330
+ sampledAt = now;
331
+ scrolledSinceUpdate = false;
332
+ if (sameState(state, next)) return;
333
+ state = next;
334
+ listener(state);
335
+ }
336
+ function currentVelocity(metrics, elapsedMs) {
337
+ if (!scrolling) return { velocityX: 0, velocityY: 0 };
338
+ if (!scrolledSinceUpdate) return { velocityX: state.velocityX, velocityY: state.velocityY };
339
+ return {
340
+ velocityX: speed(metrics.x - state.x, elapsedMs),
341
+ velocityY: speed(metrics.y - state.y, elapsedMs)
342
+ };
343
+ }
344
+ function scheduleUpdate() {
345
+ if (frame !== null) return;
346
+ frame = requestAnimationFrame(() => {
347
+ frame = null;
348
+ update();
349
+ });
350
+ }
351
+ function settle() {
352
+ scrolling = false;
353
+ update();
354
+ }
355
+ function onScroll() {
356
+ scrolling = true;
357
+ scrolledSinceUpdate = true;
358
+ clearTimeout(idleTimer);
359
+ idleTimer = setTimeout(settle, idleDelay);
360
+ scheduleUpdate();
361
+ }
362
+ target.addEventListener("scroll", onScroll, { passive: true });
363
+ const stopWatchingSize = isWindow(target) ? watchWindowSize(target, scheduleUpdate) : watchElementSize(target, scheduleUpdate);
364
+ listener(state);
365
+ return () => {
366
+ target.removeEventListener("scroll", onScroll);
367
+ stopWatchingSize();
368
+ if (frame !== null) cancelAnimationFrame(frame);
369
+ clearTimeout(idleTimer);
370
+ };
371
+ }
372
+ function describePosition(metrics) {
373
+ const { x, y, minX, maxX, maxY } = metrics;
374
+ const rangeX = maxX - minX;
375
+ return {
376
+ x,
377
+ y,
378
+ minX,
379
+ maxX,
380
+ maxY,
381
+ progressX: progress(x - minX, rangeX),
382
+ progressY: progress(y, maxY),
383
+ atTop: y <= EDGE_TOLERANCE,
384
+ atBottom: y >= maxY - EDGE_TOLERANCE,
385
+ atLeft: x <= minX + EDGE_TOLERANCE,
386
+ atRight: x >= maxX - EDGE_TOLERANCE,
387
+ canScrollX: rangeX > EDGE_TOLERANCE,
388
+ canScrollY: maxY > EDGE_TOLERANCE
389
+ };
390
+ }
391
+ function progress(offset, range) {
392
+ if (range <= EDGE_TOLERANCE) return 0;
393
+ return Math.min(1, Math.max(0, offset / range));
394
+ }
395
+ function nextState(previous, metrics, isScrolling, velocity) {
396
+ return {
397
+ ...describePosition(metrics),
398
+ directionX: directionOf(metrics.x - previous.x, previous.directionX),
399
+ directionY: directionOf(metrics.y - previous.y, previous.directionY),
400
+ ...velocity,
401
+ isScrolling
402
+ };
403
+ }
404
+ function directionOf(delta, previous) {
405
+ if (delta > 0) return 1;
406
+ if (delta < 0) return -1;
407
+ return previous;
408
+ }
409
+ function speed(delta, elapsedMs) {
410
+ return elapsedMs > 0 ? delta / elapsedMs * 1e3 : 0;
411
+ }
412
+ function sameState(a, b) {
413
+ return STATE_KEYS.every((key) => a[key] === b[key]);
414
+ }
415
+ function watchElementSize(element, onResize) {
416
+ const resizeObserver = new ResizeObserver(onResize);
417
+ resizeObserver.observe(element);
418
+ for (const child of element.children) resizeObserver.observe(child);
419
+ const mutationObserver = new MutationObserver((records) => {
420
+ let changed = false;
421
+ for (const record of records) {
422
+ if (record.type === "characterData") {
423
+ if (record.target.parentNode === element) changed = true;
424
+ continue;
425
+ }
426
+ if (record.target !== element) continue;
427
+ changed = true;
428
+ for (const node of record.addedNodes) {
429
+ if (node instanceof Element) resizeObserver.observe(node);
430
+ }
431
+ for (const node of record.removedNodes) {
432
+ if (node instanceof Element) resizeObserver.unobserve(node);
433
+ }
434
+ }
435
+ if (changed) onResize();
436
+ });
437
+ mutationObserver.observe(element, { childList: true, subtree: true, characterData: true });
438
+ return () => {
439
+ resizeObserver.disconnect();
440
+ mutationObserver.disconnect();
441
+ };
442
+ }
443
+ function watchWindowSize(win, onResize) {
444
+ const { documentElement, body } = win.document;
445
+ const resizeObserver = new ResizeObserver(onResize);
446
+ resizeObserver.observe(documentElement);
447
+ resizeObserver.observe(body);
448
+ win.addEventListener("resize", onResize);
449
+ return () => {
450
+ resizeObserver.disconnect();
451
+ win.removeEventListener("resize", onResize);
452
+ };
453
+ }
454
+
455
+ // src/core/lock-scroll.ts
456
+ var activeLocks = /* @__PURE__ */ new Set();
457
+ var containedElements = /* @__PURE__ */ new Map();
458
+ var unlockPage = null;
459
+ function lockScroll({ allow = [] } = {}) {
460
+ const lock = { allow: [...allow] };
461
+ if (activeLocks.size === 0) unlockPage = lockPage();
462
+ activeLocks.add(lock);
463
+ syncContainment();
464
+ return () => {
465
+ if (!activeLocks.delete(lock)) return;
466
+ syncContainment();
467
+ if (activeLocks.size === 0) {
468
+ unlockPage == null ? void 0 : unlockPage();
469
+ unlockPage = null;
470
+ }
471
+ };
472
+ }
473
+ function lockPage() {
474
+ const { documentElement, body } = document;
475
+ const restoreBody = reservesScrollbarGutter(documentElement) ? noop : compensateScrollbar(documentElement, body);
476
+ const restoreRoot = setInlineStyles(documentElement, {
477
+ "overflow-x": "hidden",
478
+ "overflow-y": "hidden"
479
+ });
480
+ const stopGuarding = guardTouchScroll();
481
+ return () => {
482
+ stopGuarding();
483
+ restoreBody();
484
+ restoreRoot();
485
+ };
486
+ }
487
+ function reservesScrollbarGutter(root) {
488
+ return getComputedStyle(root).getPropertyValue("scrollbar-gutter").includes("stable");
489
+ }
490
+ function compensateScrollbar(root, body) {
491
+ const scrollbarWidth = window.innerWidth - root.clientWidth;
492
+ const paddingRight = parseFloat(getComputedStyle(body).paddingRight) + scrollbarWidth;
493
+ return setInlineStyles(body, { "padding-right": `${paddingRight}px` });
494
+ }
495
+ function noop() {
496
+ }
497
+ function syncContainment() {
498
+ const allowed = allowedElements();
499
+ for (const [element, restore] of containedElements) {
500
+ if (allowed.has(element)) continue;
501
+ restore();
502
+ containedElements.delete(element);
503
+ }
504
+ for (const element of allowed) {
505
+ if (containedElements.has(element)) continue;
506
+ containedElements.set(
507
+ element,
508
+ setInlineStyles(element, {
509
+ "overscroll-behavior-x": "contain",
510
+ "overscroll-behavior-y": "contain"
511
+ })
512
+ );
513
+ }
514
+ }
515
+ function allowedElements() {
516
+ const allowed = /* @__PURE__ */ new Set();
517
+ for (const lock of activeLocks) {
518
+ for (const element of lock.allow) allowed.add(element);
519
+ }
520
+ return allowed;
521
+ }
522
+ function guardTouchScroll() {
523
+ let previous = null;
524
+ function onTouchStart(event) {
525
+ previous = touchPoint(event);
526
+ }
527
+ function onTouchMove(event) {
528
+ const current = touchPoint(event);
529
+ const delta = previous && current ? { x: previous.x - current.x, y: previous.y - current.y } : null;
530
+ previous = current;
531
+ if (event.touches.length > 1) return;
532
+ if (!delta || !canScrollInside(event.target, delta)) event.preventDefault();
533
+ }
534
+ document.addEventListener("touchstart", onTouchStart, { passive: true });
535
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
536
+ return () => {
537
+ document.removeEventListener("touchstart", onTouchStart);
538
+ document.removeEventListener("touchmove", onTouchMove);
539
+ };
540
+ }
541
+ function touchPoint(event) {
542
+ const touch = event.touches[0];
543
+ return touch ? { x: touch.clientX, y: touch.clientY } : null;
544
+ }
545
+ function canScrollInside(target, delta) {
546
+ if (!(target instanceof Element)) return false;
547
+ const boundary = outermostAllowedAncestor(target);
548
+ if (!boundary) return false;
549
+ const axis = Math.abs(delta.x) > Math.abs(delta.y) ? "x" : "y";
550
+ const scroller = nearestScroller(target, boundary, axis);
551
+ return scroller !== null && canScrollToward(scroller, axis, delta[axis]);
552
+ }
553
+ function outermostAllowedAncestor(target) {
554
+ const allowed = allowedElements();
555
+ let outermost = null;
556
+ for (let el = target; el; el = el.parentElement) {
557
+ if (el instanceof HTMLElement && allowed.has(el)) outermost = el;
558
+ }
559
+ return outermost;
560
+ }
561
+ function nearestScroller(target, boundary, axis) {
562
+ for (let el = target; el; el = el.parentElement) {
563
+ if (el instanceof HTMLElement && isScrollable(el, axis)) return el;
564
+ if (el === boundary) return null;
565
+ }
566
+ return null;
567
+ }
568
+ function canScrollToward(scroller, axis, delta) {
569
+ const { x, y, minX, maxX, maxY } = readScroll(scroller);
570
+ const [position, min, max] = axis === "x" ? [x, minX, maxX] : [y, 0, maxY];
571
+ if (delta > 0) return position < max - EDGE_TOLERANCE;
572
+ if (delta < 0) return position > min + EDGE_TOLERANCE;
573
+ return false;
574
+ }
575
+
576
+ // src/core/fling.ts
577
+ var DECELERATION_PER_MS = 0.998;
578
+ var MIN_FLING_VELOCITY = 50;
579
+ var FRAME_MS = 1e3 / 60;
580
+ var EASE_OUT_CUBIC_INITIAL_SLOPE = 3;
581
+ function projectMomentum(velocity) {
582
+ return velocity / 1e3 * DECELERATION_PER_MS / (1 - DECELERATION_PER_MS);
583
+ }
584
+ function fling(target, axis, velocity) {
585
+ if (!(Math.abs(velocity) >= MIN_FLING_VELOCITY)) return null;
586
+ const metrics = readScroll(target);
587
+ const current = axis === "x" ? metrics.x : metrics.y;
588
+ const destination = clampToRange(metrics, axis, current + projectMomentum(velocity));
589
+ const duration = Math.max(
590
+ FRAME_MS,
591
+ EASE_OUT_CUBIC_INITIAL_SLOPE * Math.abs(destination - current) / Math.abs(velocity) * 1e3
592
+ );
593
+ return animateScroll(target, axis === "x" ? { x: destination } : { y: destination }, {
594
+ animation: { type: "tween", duration, easing: easings.easeOutCubic },
595
+ // Momentum carries on the user's own gesture, which native scrolling keeps under reduced motion.
596
+ respectReducedMotion: false
597
+ });
598
+ }
599
+
600
+ // src/core/snap.ts
601
+ var FLICK_VELOCITY = 300;
602
+ function readSnapPositions(container, axis) {
603
+ const containerStyle = getComputedStyle(container);
604
+ if (containerStyle.scrollSnapType === "none") return [];
605
+ const rightToLeft = axis === "x" && containerStyle.direction === "rtl";
606
+ const port = scrollport(container, axis);
607
+ const origin = contentOrigin(container, axis);
608
+ const metrics = readScroll(container);
609
+ const positions = /* @__PURE__ */ new Set();
610
+ for (const [child, style] of snapAreaCandidates(container)) {
611
+ const logical = snapAlignment(style.scrollSnapAlign, axis);
612
+ if (logical === null) continue;
613
+ const alignment = rightToLeft ? mirrored(logical) : logical;
614
+ const area = snapArea(child, style, origin, axis);
615
+ positions.add(clampToRange(metrics, axis, alignedPosition(area, port, alignment)));
616
+ }
617
+ return [...positions].sort((a, b) => a - b);
618
+ }
619
+ function* snapAreaCandidates(root) {
620
+ for (const child of root.children) {
621
+ const style = getComputedStyle(child);
622
+ yield [child, style];
623
+ if (style.scrollSnapType === "none") yield* snapAreaCandidates(child);
624
+ }
625
+ }
626
+ function pickSnapTarget(positions, current, projected, velocity) {
627
+ if (positions.length === 0) throw new RangeError("pickSnapTarget: positions must not be empty");
628
+ const nearest = positions.reduce(
629
+ (best, position) => Math.abs(position - projected) < Math.abs(best - projected) ? position : best
630
+ );
631
+ if (Math.abs(velocity) < FLICK_VELOCITY) return nearest;
632
+ const direction = Math.sign(velocity);
633
+ const isAhead = (position) => (position - current) * direction > EDGE_TOLERANCE;
634
+ if (isAhead(nearest)) return nearest;
635
+ const ahead = positions.filter(isAhead);
636
+ if (ahead.length === 0) return nearest;
637
+ return direction > 0 ? ahead[0] : ahead[ahead.length - 1];
638
+ }
639
+ function snapAlignment(value, axis) {
640
+ const [block, inline = block] = value.split(" ");
641
+ const keyword = axis === "x" ? inline : block;
642
+ if (keyword === "start" || keyword === "center" || keyword === "end") return keyword;
643
+ return null;
644
+ }
645
+ function mirrored(alignment) {
646
+ if (alignment === "start") return "end";
647
+ if (alignment === "end") return "start";
648
+ return alignment;
649
+ }
650
+ function scrollport(container, axis) {
651
+ const style = getComputedStyle(container);
652
+ const size = axis === "x" ? container.clientWidth : container.clientHeight;
653
+ const [paddingStart, paddingEnd] = axis === "x" ? [style.scrollPaddingLeft, style.scrollPaddingRight] : [style.scrollPaddingTop, style.scrollPaddingBottom];
654
+ return { start: resolvePadding(paddingStart, size), end: resolvePadding(paddingEnd, size), size };
655
+ }
656
+ function resolvePadding(value, size) {
657
+ if (value === "auto") return 0;
658
+ if (value.endsWith("%")) return parseFloat(value) / 100 * size;
659
+ return parseFloat(value);
660
+ }
661
+ function contentOrigin(container, axis) {
662
+ const box = container.getBoundingClientRect();
663
+ if (axis === "x") return box.left + container.clientLeft - container.scrollLeft;
664
+ return box.top + container.clientTop - container.scrollTop;
665
+ }
666
+ function snapArea(child, style, origin, axis) {
667
+ const box = child.getBoundingClientRect();
668
+ if (axis === "x") {
669
+ return {
670
+ start: box.left - parseFloat(style.scrollMarginLeft) - origin,
671
+ end: box.right + parseFloat(style.scrollMarginRight) - origin
672
+ };
673
+ }
674
+ return {
675
+ start: box.top - parseFloat(style.scrollMarginTop) - origin,
676
+ end: box.bottom + parseFloat(style.scrollMarginBottom) - origin
677
+ };
678
+ }
679
+ function alignedPosition(area, port, alignment) {
680
+ switch (alignment) {
681
+ case "start":
682
+ return area.start - port.start;
683
+ case "end":
684
+ return area.end - (port.size - port.end);
685
+ case "center":
686
+ return (area.start + area.end) / 2 - (port.start + (port.size - port.start - port.end) / 2);
687
+ default: {
688
+ const exhaustive = alignment;
689
+ return exhaustive;
690
+ }
691
+ }
692
+ }
693
+
694
+ // src/core/velocity.ts
695
+ var VELOCITY_WINDOW_MS = 100;
696
+ function recordSample(samples, sample2) {
697
+ samples.push(sample2);
698
+ while (sample2.time - samples[0].time > VELOCITY_WINDOW_MS) samples.shift();
699
+ }
700
+ function releaseVelocity(samples) {
701
+ const first = samples[0];
702
+ const last = samples[samples.length - 1];
703
+ const elapsed = last.time - first.time;
704
+ return elapsed > 0 ? (last.position - first.position) / elapsed * 1e3 : 0;
705
+ }
706
+
707
+ // src/core/drag-scroll.ts
708
+ var DRAG_SLOP = 8;
709
+ var CLICK_AFTER_DRAG_MS = 100;
710
+ var NATIVE_PAN = { x: "pan-y pinch-zoom", y: "pan-x pinch-zoom" };
711
+ function dragScroll(element, { axis = "x", mouse = false } = {}) {
712
+ const restoreTouchAction = setInlineStyles(element, { "touch-action": NATIVE_PAN[axis] });
713
+ let gesture = null;
714
+ let release = null;
715
+ let suppressClicksUntil = -Infinity;
716
+ function accepts(event) {
717
+ if (!event.isPrimary) return false;
718
+ if (event.pointerType === "touch") return true;
719
+ return mouse && event.pointerType === "mouse" && event.button === 0;
720
+ }
721
+ function onPointerDown(event) {
722
+ suppressClicksUntil = -Infinity;
723
+ if (!accepts(event)) return;
724
+ endGesture(0);
725
+ gesture = {
726
+ kind: "pending",
727
+ pointerId: event.pointerId,
728
+ pointerType: event.pointerType,
729
+ origin: pointFrom(event),
730
+ snap: interruptRelease()
731
+ };
732
+ }
733
+ function interruptRelease() {
734
+ if (!release) return null;
735
+ const { handle, positions } = release;
736
+ handle.cancel();
737
+ return { positions, restore: suspendSnap2() };
738
+ }
739
+ function suspendSnap2() {
740
+ return setInlineStyles(element, { "scroll-snap-type": "none" });
741
+ }
742
+ function onPointerMove(event) {
743
+ if ((gesture == null ? void 0 : gesture.pointerId) !== event.pointerId) return;
744
+ if (gesture.kind === "pending") {
745
+ if (!isPressed(event)) {
746
+ endGesture(0);
747
+ return;
748
+ }
749
+ const intent = judgeIntent(gesture.origin, pointFrom(event), axis);
750
+ if (intent === "cross-axis") endGesture(0);
751
+ if (intent === "along-axis") gesture = startDrag(gesture, event);
752
+ return;
753
+ }
754
+ const position = dragPosition(gesture, event);
755
+ recordSample(gesture.samples, { time: event.timeStamp, position });
756
+ writeScroll(element, axis === "x" ? { x: position } : { y: position });
757
+ }
758
+ function dragPosition(drag, event) {
759
+ return drag.startScroll - (alongAxis(event, axis) - drag.origin);
760
+ }
761
+ function startDrag(pending, event) {
762
+ var _a;
763
+ element.setPointerCapture(event.pointerId);
764
+ cancelScroll(element);
765
+ const snap = (_a = pending.snap) != null ? _a : {
766
+ positions: readSnapPositions(element, axis),
767
+ restore: suspendSnap2()
768
+ };
769
+ const startScroll = readScroll(element)[axis];
770
+ return {
771
+ kind: "dragging",
772
+ pointerId: event.pointerId,
773
+ pointerType: event.pointerType,
774
+ origin: alongAxis(event, axis),
775
+ startScroll,
776
+ samples: [{ time: event.timeStamp, position: startScroll }],
777
+ snap,
778
+ restoreUserSelect: event.pointerType === "mouse" ? setInlineStyles(element, { "user-select": "none", "-webkit-user-select": "none" }) : noop2
779
+ };
780
+ }
781
+ function onPointerUp(event) {
782
+ if ((gesture == null ? void 0 : gesture.pointerId) !== event.pointerId) return;
783
+ if (gesture.kind === "pending") {
784
+ endGesture(0);
785
+ return;
786
+ }
787
+ recordSample(gesture.samples, { time: event.timeStamp, position: dragPosition(gesture, event) });
788
+ suppressClicksUntil = event.timeStamp + CLICK_AFTER_DRAG_MS;
789
+ endGesture(releaseVelocity(gesture.samples));
790
+ }
791
+ function onPointerCancel(event) {
792
+ if ((gesture == null ? void 0 : gesture.pointerId) === event.pointerId) endGesture(0);
793
+ }
794
+ function endGesture(velocity) {
795
+ const ended = gesture;
796
+ gesture = null;
797
+ if ((ended == null ? void 0 : ended.kind) === "dragging") ended.restoreUserSelect();
798
+ if (ended == null ? void 0 : ended.snap) settle(ended.snap, velocity);
799
+ }
800
+ function settle({ positions, restore }, velocity) {
801
+ if (positions.length === 0) {
802
+ restore();
803
+ track(fling(element, axis, velocity), positions);
804
+ return;
805
+ }
806
+ const current = readScroll(element)[axis];
807
+ const target = pickSnapTarget(positions, current, current + projectMomentum(velocity), velocity);
808
+ restore();
809
+ const handle = animateScroll(element, axis === "x" ? { x: target } : { y: target }, {
810
+ animation: { type: "spring", velocity: axis === "x" ? { x: velocity } : { y: velocity } },
811
+ respectReducedMotion: false
812
+ });
813
+ track(handle, positions);
814
+ }
815
+ function track(handle, positions) {
816
+ if (!handle) return;
817
+ const current = { handle, positions };
818
+ release = current;
819
+ void handle.finished.then(() => {
820
+ if (release === current) release = null;
821
+ });
822
+ }
823
+ function onDragStart(event) {
824
+ if ((gesture == null ? void 0 : gesture.pointerType) === "mouse") event.preventDefault();
825
+ }
826
+ function onClick(event) {
827
+ if (event.timeStamp > suppressClicksUntil) return;
828
+ suppressClicksUntil = -Infinity;
829
+ event.preventDefault();
830
+ event.stopPropagation();
831
+ }
832
+ element.addEventListener("pointerdown", onPointerDown);
833
+ element.addEventListener("pointermove", onPointerMove);
834
+ element.addEventListener("pointerup", onPointerUp);
835
+ element.addEventListener("pointercancel", onPointerCancel);
836
+ element.addEventListener("dragstart", onDragStart);
837
+ element.addEventListener("click", onClick, { capture: true });
838
+ return () => {
839
+ var _a;
840
+ element.removeEventListener("pointerdown", onPointerDown);
841
+ element.removeEventListener("pointermove", onPointerMove);
842
+ element.removeEventListener("pointerup", onPointerUp);
843
+ element.removeEventListener("pointercancel", onPointerCancel);
844
+ element.removeEventListener("dragstart", onDragStart);
845
+ element.removeEventListener("click", onClick, { capture: true });
846
+ if ((gesture == null ? void 0 : gesture.kind) === "dragging") gesture.restoreUserSelect();
847
+ (_a = gesture == null ? void 0 : gesture.snap) == null ? void 0 : _a.restore();
848
+ gesture = null;
849
+ release == null ? void 0 : release.handle.cancel();
850
+ release = null;
851
+ restoreTouchAction();
852
+ };
853
+ }
854
+ function judgeIntent(origin, point, axis) {
855
+ const dx = point.x - origin.x;
856
+ const dy = point.y - origin.y;
857
+ if (Math.hypot(dx, dy) < DRAG_SLOP) return "undecided";
858
+ const [along, cross] = axis === "x" ? [dx, dy] : [dy, dx];
859
+ return Math.abs(cross) > Math.abs(along) ? "cross-axis" : "along-axis";
860
+ }
861
+ function isPressed(event) {
862
+ return (event.buttons & 1) === 1;
863
+ }
864
+ function pointFrom(event) {
865
+ return { x: event.clientX, y: event.clientY };
866
+ }
867
+ function alongAxis(event, axis) {
868
+ return axis === "x" ? event.clientX : event.clientY;
869
+ }
870
+ function noop2() {
871
+ }
872
+
873
+ // src/core/scroll-to.ts
874
+ var EDGE_DESTINATIONS = {
875
+ top: { y: -Infinity },
876
+ bottom: { y: Infinity },
877
+ left: { x: -Infinity },
878
+ right: { x: Infinity }
879
+ };
880
+ function scrollToEdge(target, edge, options) {
881
+ return animateScroll(target, EDGE_DESTINATIONS[edge], options);
882
+ }
883
+ function scrollBy(target, delta, options) {
884
+ const base = scrollDestination(target);
885
+ return animateScroll(
886
+ target,
887
+ {
888
+ x: delta.x === void 0 ? void 0 : base.x + delta.x,
889
+ y: delta.y === void 0 ? void 0 : base.y + delta.y
890
+ },
891
+ options
892
+ );
893
+ }
894
+ function scrollToElement(target, element, options = {}) {
895
+ if (!isWindow(target) && !target.contains(element)) {
896
+ throw new Error("scrollToElement: element is not inside the scroll target");
897
+ }
898
+ const { align = "start", offset = 0, ...animateOptions } = options;
899
+ const metrics = readScroll(target);
900
+ const viewport = viewportOrigin(target);
901
+ const rect = element.getBoundingClientRect();
902
+ const x = placeOnAxis(align, {
903
+ elementStart: rect.left - viewport.left,
904
+ elementSize: rect.width,
905
+ viewportSize: metrics.viewportWidth,
906
+ scroll: metrics.x,
907
+ inset: 0
908
+ });
909
+ const y = placeOnAxis(align, {
910
+ elementStart: rect.top - viewport.top,
911
+ elementSize: rect.height,
912
+ viewportSize: metrics.viewportHeight,
913
+ scroll: metrics.y,
914
+ inset: offset
915
+ });
916
+ return animateScroll(target, { x, y }, animateOptions);
917
+ }
918
+ function viewportOrigin(target) {
919
+ if (isWindow(target)) return { left: 0, top: 0 };
920
+ const rect = target.getBoundingClientRect();
921
+ return { left: rect.left + target.clientLeft, top: rect.top + target.clientTop };
922
+ }
923
+ function placeOnAxis(align, placement) {
924
+ const { elementStart, elementSize, viewportSize, scroll, inset } = placement;
925
+ const start = scroll + elementStart - inset;
926
+ const end = scroll + elementStart + elementSize - viewportSize;
927
+ switch (align) {
928
+ case "start":
929
+ return start;
930
+ case "end":
931
+ return end;
932
+ case "center":
933
+ return scroll + elementStart + elementSize / 2 - (inset + viewportSize) / 2;
934
+ case "nearest": {
935
+ const fullyVisible = elementStart >= inset && elementStart + elementSize <= viewportSize;
936
+ if (fullyVisible) return void 0;
937
+ return Math.abs(start - scroll) <= Math.abs(end - scroll) ? start : end;
938
+ }
939
+ default: {
940
+ const exhaustive = align;
941
+ throw new Error(`scrollToElement: unknown align ${String(exhaustive)}`);
942
+ }
943
+ }
944
+ }
945
+
946
+ // src/core/active-section.ts
947
+ function pickActiveSection(sectionTops, line, atEnd) {
948
+ let active = -1;
949
+ sectionTops.forEach((top, index) => {
950
+ if (top === null) return;
951
+ if (atEnd || top <= line + EDGE_TOLERANCE) active = index;
952
+ });
953
+ return active;
954
+ }
955
+ function observeActiveSection(sections, listener, { root = window, offset = 0 } = {}) {
956
+ if (!Number.isFinite(offset)) {
957
+ throw new RangeError(`observeActiveSection: offset must be a finite number, got ${offset}`);
958
+ }
959
+ if (!isWindow(root) && !sections.every((section) => root.contains(section))) {
960
+ throw new Error("observeActiveSection: every section must be inside the root");
961
+ }
962
+ let atEnd = false;
963
+ let active;
964
+ function update() {
965
+ const line = viewportOrigin(root).top + offset;
966
+ const tops = sections.map(layoutTop);
967
+ const next = pickActiveSection(tops, line, atEnd);
968
+ if (next === active) return;
969
+ active = next;
970
+ listener(next);
971
+ }
972
+ const resizeObserver = new ResizeObserver(update);
973
+ for (const section of sections) resizeObserver.observe(section);
974
+ const stopObservingScroll = observeScroll(root, (state) => {
975
+ atEnd = state.canScrollY && state.atBottom;
976
+ update();
977
+ });
978
+ return () => {
979
+ stopObservingScroll();
980
+ resizeObserver.disconnect();
981
+ };
982
+ }
983
+ function layoutTop(element) {
984
+ if (element.getClientRects().length === 0) return null;
985
+ return element.getBoundingClientRect().top;
986
+ }
987
+
988
+ // src/core/restore-scroll.ts
989
+ var INTERACTION_EVENTS = ["wheel", "touchstart", "pointerdown", "keydown"];
990
+ var MAX_TIMEOUT_MS2 = 2147483647;
991
+ var historyHolds = /* @__PURE__ */ new Set();
992
+ var originalHistoryRestoration = "auto";
993
+ function restoreScroll(target, key, { storage, timeout = 3e3 } = {}) {
994
+ if (!(timeout >= 0 && timeout <= MAX_TIMEOUT_MS2)) {
995
+ throw new RangeError(
996
+ `restoreScroll: timeout must be between 0 and ${MAX_TIMEOUT_MS2} ms, got ${timeout}`
997
+ );
998
+ }
999
+ const storageKey = `use-scroller:${key}`;
1000
+ let store;
1001
+ let saved;
1002
+ try {
1003
+ store = storage != null ? storage : window.sessionStorage;
1004
+ saved = parsePosition(store.getItem(storageKey));
1005
+ } catch {
1006
+ return noop3;
1007
+ }
1008
+ let unsavedPosition = null;
1009
+ let frame = null;
1010
+ let saving = false;
1011
+ function save() {
1012
+ frame = null;
1013
+ const position = unsavedPosition;
1014
+ unsavedPosition = null;
1015
+ if (!position) return;
1016
+ try {
1017
+ store.setItem(storageKey, JSON.stringify(position));
1018
+ } catch {
1019
+ }
1020
+ }
1021
+ function onScroll() {
1022
+ unsavedPosition = scrollPosition(target);
1023
+ if (frame === null) frame = requestAnimationFrame(save);
1024
+ }
1025
+ function flush() {
1026
+ if (frame !== null) cancelAnimationFrame(frame);
1027
+ save();
1028
+ }
1029
+ function startSaving() {
1030
+ saving = true;
1031
+ target.addEventListener("scroll", onScroll, { passive: true });
1032
+ window.addEventListener("pagehide", flush);
1033
+ }
1034
+ const releaseHistory = isWindow(target) ? takeOverHistoryRestoration(target.history) : noop3;
1035
+ const stopRestoring = saved ? restoreWhenReachable(target, saved, timeout, startSaving) : noop3;
1036
+ if (!saved) startSaving();
1037
+ return () => {
1038
+ stopRestoring();
1039
+ if (saving) {
1040
+ target.removeEventListener("scroll", onScroll);
1041
+ window.removeEventListener("pagehide", flush);
1042
+ flush();
1043
+ }
1044
+ releaseHistory();
1045
+ };
1046
+ }
1047
+ function restoreWhenReachable(target, position, timeout, onSettled) {
1048
+ let waiting = true;
1049
+ let stopObserving = noop3;
1050
+ const timer = setTimeout(() => {
1051
+ writeScroll(target, position);
1052
+ settle();
1053
+ }, timeout);
1054
+ function stop() {
1055
+ waiting = false;
1056
+ stopObserving();
1057
+ clearTimeout(timer);
1058
+ for (const type of INTERACTION_EVENTS) target.removeEventListener(type, settle);
1059
+ }
1060
+ function settle() {
1061
+ stop();
1062
+ onSettled();
1063
+ }
1064
+ function restoreIfReachable(range) {
1065
+ if (!waiting || !isReachable(range, position)) return;
1066
+ writeScroll(target, position);
1067
+ settle();
1068
+ }
1069
+ for (const type of INTERACTION_EVENTS) {
1070
+ target.addEventListener(type, settle, { passive: true });
1071
+ }
1072
+ stopObserving = observeScroll(target, restoreIfReachable);
1073
+ if (!waiting) stopObserving();
1074
+ return stop;
1075
+ }
1076
+ function takeOverHistoryRestoration(history) {
1077
+ const hold = {};
1078
+ if (historyHolds.size === 0) {
1079
+ originalHistoryRestoration = history.scrollRestoration;
1080
+ history.scrollRestoration = "manual";
1081
+ }
1082
+ historyHolds.add(hold);
1083
+ return () => {
1084
+ if (!historyHolds.delete(hold)) return;
1085
+ if (historyHolds.size === 0) history.scrollRestoration = originalHistoryRestoration;
1086
+ };
1087
+ }
1088
+ function isReachable(range, position) {
1089
+ return position.y <= range.maxY + EDGE_TOLERANCE && position.x >= range.minX - EDGE_TOLERANCE && position.x <= range.maxX + EDGE_TOLERANCE;
1090
+ }
1091
+ function scrollPosition(target) {
1092
+ if (isWindow(target)) return { x: target.scrollX, y: target.scrollY };
1093
+ return { x: target.scrollLeft, y: target.scrollTop };
1094
+ }
1095
+ function parsePosition(json) {
1096
+ if (json === null) return null;
1097
+ let value;
1098
+ try {
1099
+ value = JSON.parse(json);
1100
+ } catch {
1101
+ return null;
1102
+ }
1103
+ return isPosition(value) ? { x: value.x, y: value.y } : null;
1104
+ }
1105
+ function isPosition(value) {
1106
+ return typeof value === "object" && value !== null && "x" in value && "y" in value && Number.isFinite(value.x) && Number.isFinite(value.y);
1107
+ }
1108
+ function noop3() {
1109
+ }
1110
+
1111
+ // src/core/stick-to-bottom.ts
1112
+ var STICK_THRESHOLD = 8;
1113
+ function stickToBottom(element, onChange) {
1114
+ let stuck = true;
1115
+ let stopped = false;
1116
+ let ownAnimation = null;
1117
+ let previous = jumpToBottom(element);
1118
+ function setStuck(next) {
1119
+ if (next === stuck) return;
1120
+ stuck = next;
1121
+ onChange == null ? void 0 : onChange(stuck);
1122
+ }
1123
+ onChange == null ? void 0 : onChange(stuck);
1124
+ const unobserve = observeScroll(element, (current) => {
1125
+ const nextStuck = stuckAfter(stuck, previous, current);
1126
+ const follow = nextStuck && !ownAnimation && grewWithoutMovingUp(previous, current);
1127
+ previous = follow ? jumpToBottom(element) : { y: current.y, maxY: current.maxY };
1128
+ setStuck(nextStuck);
1129
+ });
1130
+ return {
1131
+ scrollToBottom(options) {
1132
+ if (stopped) throw new Error("stickToBottom: scrollToBottom called after stop()");
1133
+ const handle = animateScroll(element, { y: Infinity }, options);
1134
+ ownAnimation = handle;
1135
+ setStuck(true);
1136
+ void handle.finished.then((result) => {
1137
+ if (ownAnimation !== handle) return;
1138
+ ownAnimation = null;
1139
+ if (result !== "completed") setStuck(isNearBottom(readScroll(element)));
1140
+ });
1141
+ return handle;
1142
+ },
1143
+ stop() {
1144
+ stopped = true;
1145
+ const animation = ownAnimation;
1146
+ ownAnimation = null;
1147
+ animation == null ? void 0 : animation.cancel();
1148
+ unobserve();
1149
+ }
1150
+ };
1151
+ }
1152
+ function jumpToBottom(element) {
1153
+ writeScroll(element, { y: readScroll(element).maxY });
1154
+ const { y, maxY } = readScroll(element);
1155
+ return { y, maxY };
1156
+ }
1157
+ function isNearBottom({ y, maxY }) {
1158
+ return maxY - y <= STICK_THRESHOLD;
1159
+ }
1160
+ function stuckAfter(stuck, previous, current) {
1161
+ if (isNearBottom(current)) return true;
1162
+ if (current.y < previous.y) return false;
1163
+ return stuck;
1164
+ }
1165
+ function grewWithoutMovingUp(previous, current) {
1166
+ return current.maxY > previous.maxY && current.y >= previous.y;
1167
+ }
1168
+
1169
+ export {
1170
+ isWindow,
1171
+ readScroll,
1172
+ isScrollable,
1173
+ findScrollParent,
1174
+ easings,
1175
+ cubicBezier,
1176
+ animateScroll,
1177
+ cancelScroll,
1178
+ initialScrollState,
1179
+ observeScroll,
1180
+ lockScroll,
1181
+ dragScroll,
1182
+ scrollToEdge,
1183
+ scrollBy,
1184
+ scrollToElement,
1185
+ observeActiveSection,
1186
+ restoreScroll,
1187
+ stickToBottom
1188
+ };