scroll-arrows 0.1.3 → 0.2.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.
package/README.md CHANGED
@@ -57,13 +57,94 @@ arrow.destroy();
57
57
  - **Obstacle routing** — pass `avoid` (an element or array) and the curve bows
58
58
  around them with an `avoidPadding` gap instead of cutting through. Single-bend
59
59
  router: it clears the worst blocker, not a full path-finder.
60
+ - **Shared-origin fan-out** — when several arrows leave the same element they all
61
+ resolve to the same edge point and stack. Slide each along its edge with
62
+ `startSocketOffset` / `endSocketOffset` (a fraction of the edge length, `0` =
63
+ centered, `±0.5` = corners) to spread them — e.g. `-0.25`, `0`, `+0.25` for
64
+ three siblings off one parent.
65
+ - **Elbow routing** — set `route: 'elbow'` for right-angle connectors (the
66
+ classic tree / org-chart bracket) instead of a smooth curve. Same-axis sockets
67
+ get a centered Z-bracket, perpendicular sockets a single L-corner. Pair with
68
+ explicit `startSocket` / `endSocket` for predictable shapes. Elbow mode ignores
69
+ `avoid` and `curvature`.
70
+ - **Hidden anchors (tabs / accordions)** — an anchor inside a `display:none`
71
+ container has no box, so the arrow can't be drawn yet. Instead of rendering a
72
+ collapsed/garbage line, it draws nothing and **auto-redraws the moment the
73
+ anchor is revealed** (via `IntersectionObserver`). For full control — or for
74
+ engines without `IntersectionObserver` — call `arrow.refresh()` from your
75
+ tab/accordion show handler:
76
+
77
+ ```ts
78
+ const arrow = scrollArrow({ start: '#a', end: '#tab-2-target' });
79
+
80
+ tabButton.addEventListener('click', () => {
81
+ showTabPanel(2); // your code reveals the panel
82
+ arrow.refresh(); // recompute now that the anchor has a box
83
+ });
84
+ ```
85
+
60
86
  - **Manual mode** — `scroll: false` + `setProgress(0..1)` to drive it yourself
61
87
  (e.g. from GSAP/Motion).
88
+ - **Reduced motion** — arrows auto-respect `prefers-reduced-motion: reduce`,
89
+ rendering fully drawn and static (no scroll animation) while still tracking
90
+ layout. Opt out with `respectReducedMotion: false` to keep the animation.
91
+ Works the same for `scrollArrowGroup`.
62
92
  - **Labels** — `label` rides along the line at `labelAt` (0..1, default mid)
63
93
  and can sit off the line via `labelOffset` (perpendicular px; + = left of the
64
94
  draw direction, − = right). Fades in as the pen draws through it.
65
95
  `labelBackground` masks a gap in the line behind the text (the excalidraw
66
96
  look); style via `labelColor` / `font`.
97
+ - **Staggered groups** — `scrollArrowGroup` owns N arrows and reveals them in
98
+ sequence off one shared trigger (`A then B then C`). `stagger` (0..1) controls
99
+ overlap: `1` draws each in its own slice, `0` draws them together.
100
+
101
+ ## Groups (staggered reveal)
102
+
103
+ For diagrams (org/family trees, flows) where a set of arrows should draw in
104
+ order rather than each on its own scroll, use a group. It creates the arrows in
105
+ manual mode and drives their progress as one coordinated reveal.
106
+
107
+ ```ts
108
+ import { scrollArrowGroup } from 'scroll-arrows';
109
+
110
+ const group = scrollArrowGroup({
111
+ arrows: [
112
+ { start: '#a', end: '#b', roughness: 0.6 },
113
+ { start: '#b', end: '#c', roughness: 0.6 },
114
+ { start: '#b', end: '#d', roughness: 0.6 },
115
+ ],
116
+ stagger: 1, // 0 = all together, 1 = fully sequential (default)
117
+ scroll: { target: '#diagram' }, // shared trigger; defaults to all endpoints
118
+ });
119
+
120
+ // later
121
+ group.destroy();
122
+ ```
123
+
124
+ Each entry takes the usual per-arrow options. The group forces `scroll: false`
125
+ on each arrow and slices its own progress across them. Pass `scroll: false` to
126
+ drive the whole group yourself with `group.setProgress(0..1)`; `group.refresh()`
127
+ recomputes every arrow's geometry. The default `scroll.target` is a synthetic
128
+ rect spanning every endpoint, so the group reveals as it scrolls into view.
129
+
130
+ ## Breakpoints / responsive
131
+
132
+ When a diagram reflows on small screens (absolute overlay → vertical stack), the
133
+ arrows usually should disappear. Use `setEnabled(on)` to suspend and restore an
134
+ arrow **without tearing it down** — disabling hides it and stops all scroll work;
135
+ enabling shows it and recomputes geometry. Wire it to `matchMedia`:
136
+
137
+ ```ts
138
+ const arrow = scrollArrow({ start: '#a', end: '#b' });
139
+
140
+ const mq = window.matchMedia('(max-width: 30rem)');
141
+ const sync = () => arrow.setEnabled(!mq.matches); // off below 30rem
142
+ sync();
143
+ mq.addEventListener('change', sync);
144
+ ```
145
+
146
+ Pass `enabled: false` to start hidden. `scrollArrowGroup` has the same
147
+ `enabled` option and `setEnabled(on)`, toggling the whole set at once.
67
148
 
68
149
  ## React
69
150
 
@@ -88,6 +169,19 @@ function Diagram() {
88
169
  arrow via effect and cleans up on unmount. `useScrollArrow(opts)` is the hook
89
170
  form. Pass `deps={[...]}` to re-create when inputs change.
90
171
 
172
+ For a staggered group, `ScrollArrowGroupLines` (and the `useScrollArrowGroup`
173
+ hook) take an `arrows` array whose `start`/`end` accept refs:
174
+
175
+ ```tsx
176
+ <ScrollArrowGroupLines
177
+ arrows={[
178
+ { start: a, end: b },
179
+ { start: b, end: c },
180
+ ]}
181
+ stagger={1}
182
+ />
183
+ ```
184
+
91
185
  ## Astro
92
186
 
93
187
  The core is DOM-only, so run it in a client script (it must execute in the
@@ -112,14 +206,36 @@ For an Astro React island, use the React API and hydrate with `client:visible`:
112
206
  <Diagram client:visible />
113
207
  ```
114
208
 
209
+ ## SSR & progressive enhancement
210
+
211
+ scroll-arrows is **progressive-enhancement only by design**. The arrow is an
212
+ overlay `<svg>` created by the client script at runtime — there is no
213
+ server-rendered or build-time output. In SSG/SSR setups (Astro, Next, etc.) the
214
+ connector simply does not exist until the script runs, so:
215
+
216
+ - **No-JS / pre-hydration users see nothing** where an arrow would be. Arrows
217
+ are treated as decorative enhancement, not content.
218
+ - Don't encode meaning solely in an arrow. If a relationship must survive without
219
+ JS (accessibility, SEO, no-JS fallback), express it in the DOM too — adjacent
220
+ copy, a list, a caption, an `aria-label` — and let the arrow decorate it.
221
+ - The library ships no static snapshot. Geometry depends on the live, laid-out
222
+ positions of both anchors (and the viewport), which aren't known at build time,
223
+ so a server-rendered arrow would usually be wrong anyway.
224
+
225
+ If you genuinely need a static connector in the un-hydrated state, hand-author a
226
+ plain `<svg>` in your markup and let scroll-arrows draw over it on hydration —
227
+ the runtime arrow mounts in its own overlay and won't clash with your static one.
228
+
115
229
  ## API
116
230
 
117
231
  `scrollArrow(options)` / `new ScrollArrow(options)` → instance with
118
232
  `setProgress(p)`, `refresh()`, `destroy()`.
119
233
 
120
234
  Key options: `start`, `end`, `container`, `roughness`, `stroke`, `strokeWidth`,
121
- `seed`, `startSocket`, `endSocket`, `curvature`, `head`, `headSize`, `scroll`,
122
- `speed`, `easing`, `progress`. Full types ship with the package.
235
+ `seed`, `startSocket`, `endSocket`, `startSocketOffset`, `endSocketOffset`,
236
+ `curvature`, `route`, `head`, `headSize`, `scroll`, `speed`, `easing`,
237
+ `progress`, `enabled`. Full types ship with the package. `setEnabled(on)` toggles
238
+ an arrow (and a group) on/off without teardown.
123
239
 
124
240
  ## Develop
125
241
 
@@ -16,20 +16,24 @@ function docRect(el) {
16
16
  height: r2.height
17
17
  };
18
18
  }
19
+ function isDegenerateRect(r2) {
20
+ return r2.width <= 0 || r2.height <= 0;
21
+ }
19
22
  function center(r2) {
20
23
  return { x: r2.left + r2.width / 2, y: r2.top + r2.height / 2 };
21
24
  }
22
- function socketPoint(r2, side) {
25
+ function socketPoint(r2, side, offset = 0) {
23
26
  const c = center(r2);
27
+ const o = offset < -0.5 ? -0.5 : offset > 0.5 ? 0.5 : offset;
24
28
  switch (side) {
25
29
  case "top":
26
- return { x: c.x, y: r2.top };
30
+ return { x: c.x + o * r2.width, y: r2.top };
27
31
  case "bottom":
28
- return { x: c.x, y: r2.top + r2.height };
32
+ return { x: c.x + o * r2.width, y: r2.top + r2.height };
29
33
  case "left":
30
- return { x: r2.left, y: c.y };
34
+ return { x: r2.left, y: c.y + o * r2.height };
31
35
  case "right":
32
- return { x: r2.left + r2.width, y: c.y };
36
+ return { x: r2.left + r2.width, y: c.y + o * r2.height };
33
37
  default:
34
38
  return c;
35
39
  }
@@ -62,12 +66,12 @@ function autoSide(self, other) {
62
66
  }
63
67
  return best;
64
68
  }
65
- function resolveEndpoints(startRect, endRect, startSocket, endSocket) {
69
+ function resolveEndpoints(startRect, endRect, startSocket, endSocket, startOffset = 0, endOffset = 0) {
66
70
  const s = startSocket === "auto" ? autoSide(startRect, endRect) : startSocket;
67
71
  const e = endSocket === "auto" ? autoSide(endRect, startRect) : endSocket;
68
72
  return {
69
- start: socketPoint(startRect, s),
70
- end: socketPoint(endRect, e),
73
+ start: socketPoint(startRect, s, startOffset),
74
+ end: socketPoint(endRect, e, endOffset),
71
75
  startNormal: socketNormal(s),
72
76
  endNormal: socketNormal(e)
73
77
  };
@@ -90,6 +94,26 @@ function buildPath(ep, curvature, belly = { x: 0, y: 0 }) {
90
94
  };
91
95
  return `M ${r(start.x)} ${r(start.y)} C ${r(c1.x)} ${r(c1.y)} ${r(c2.x)} ${r(c2.y)} ${r(end.x)} ${r(end.y)}`;
92
96
  }
97
+ function buildElbowPath(ep) {
98
+ const { start: s, end: e, startNormal: sn, endNormal: en } = ep;
99
+ const dx = e.x - s.x;
100
+ const dy = e.y - s.y;
101
+ const startVertical = sn.y !== 0 || sn.x === 0 && Math.abs(dy) >= Math.abs(dx);
102
+ const endVertical = en.y !== 0 || en.x === 0 && Math.abs(dy) >= Math.abs(dx);
103
+ let pts;
104
+ if (startVertical && endVertical) {
105
+ const midY = (s.y + e.y) / 2;
106
+ pts = [s, { x: s.x, y: midY }, { x: e.x, y: midY }, e];
107
+ } else if (!startVertical && !endVertical) {
108
+ const midX = (s.x + e.x) / 2;
109
+ pts = [s, { x: midX, y: s.y }, { x: midX, y: e.y }, e];
110
+ } else if (startVertical) {
111
+ pts = [s, { x: s.x, y: e.y }, e];
112
+ } else {
113
+ pts = [s, { x: e.x, y: s.y }, e];
114
+ }
115
+ return `M ${r(pts[0].x)} ${r(pts[0].y)}` + pts.slice(1).map((p) => ` L ${r(p.x)} ${r(p.y)}`).join("");
116
+ }
93
117
  function routeOffset(start, end, obstacles, padding = 14) {
94
118
  const dx = end.x - start.x;
95
119
  const dy = end.y - start.y;
@@ -165,12 +189,29 @@ function easeInOutCubic(t) {
165
189
  function clamp01(t) {
166
190
  return t < 0 ? 0 : t > 1 ? 1 : t;
167
191
  }
192
+ function prefersReducedMotion() {
193
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
194
+ return false;
195
+ }
196
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
197
+ }
168
198
  function scrollProgress(targetRect, range) {
169
199
  const vh = window.innerHeight || 1;
170
200
  const topFrac = (targetRect.top - window.scrollY) / vh;
171
201
  const [enter, leave] = range;
172
202
  return clamp01((enter - topFrac) / (enter - leave || 1));
173
203
  }
204
+ function staggerWindows(n, stagger) {
205
+ if (n <= 0) return [];
206
+ const s = clamp01(stagger);
207
+ const span = 1 / (1 + (n - 1) * s);
208
+ const step = span * s;
209
+ return Array.from({ length: n }, (_, i) => ({ start: i * step, span }));
210
+ }
211
+ function windowProgress(p, w) {
212
+ if (w.span <= 0) return p > w.start ? 1 : 0;
213
+ return clamp01((p - w.start) / w.span);
214
+ }
174
215
  function midpointRect(a, b) {
175
216
  const ra = docRect(a);
176
217
  const rb = docRect(b);
@@ -328,14 +369,36 @@ var ScrollArrow = class {
328
369
  this.svg = getOverlay(this.container);
329
370
  this.rc = rough.svg(this.svg);
330
371
  this.svg.appendChild(this.group);
331
- this.progress = clamp01(this.opts.progress);
372
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
373
+ this.progress = this.reducedMotion ? 1 : clamp01(this.opts.progress);
332
374
  this.refs = this.resolveRefs();
333
375
  this.stroke = options.stroke ?? getComputedStyle(this.container).color ?? "#222";
334
376
  this.seed = options.seed ?? deriveSeed(refKey(options.start), refKey(options.end));
377
+ this.enabled = this.opts.enabled ?? true;
378
+ if (!this.enabled) this.group.style.display = "none";
335
379
  this.render();
336
380
  this.bind();
337
381
  this.update();
338
382
  }
383
+ /**
384
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
385
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
386
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
387
+ * to `matchMedia` to switch arrows off below a breakpoint.
388
+ */
389
+ setEnabled(on) {
390
+ if (on === this.enabled || this.destroyed) return;
391
+ this.enabled = on;
392
+ if (on) {
393
+ this.group.style.display = "";
394
+ this.render();
395
+ this.update();
396
+ } else {
397
+ this.group.style.display = "none";
398
+ cancelAnimationFrame(this.rafId);
399
+ this.rafId = 0;
400
+ }
401
+ }
339
402
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
340
403
  setProgress(p) {
341
404
  this.progress = clamp01(p);
@@ -349,6 +412,7 @@ var ScrollArrow = class {
349
412
  destroy() {
350
413
  this.destroyed = true;
351
414
  this.ro?.disconnect();
415
+ this.teardownReveal();
352
416
  window.removeEventListener("scroll", this.onScroll, true);
353
417
  window.removeEventListener("resize", this.onScroll);
354
418
  cancelAnimationFrame(this.rafId);
@@ -374,17 +438,30 @@ var ScrollArrow = class {
374
438
  const list = Array.isArray(a) ? a : [a];
375
439
  return list.map(resolve).filter((el) => el !== null);
376
440
  }
377
- computeEndpoints() {
378
- const sr = docRect(this.refs.start);
379
- const er = docRect(this.refs.end);
441
+ computeEndpoints(sr, er) {
380
442
  const ss = this.opts.startSocket ?? "auto";
381
443
  const es = this.opts.endSocket ?? "auto";
382
- return resolveEndpoints(sr, er, ss, es);
444
+ return resolveEndpoints(
445
+ sr,
446
+ er,
447
+ ss,
448
+ es,
449
+ this.opts.startSocketOffset ?? 0,
450
+ this.opts.endSocketOffset ?? 0
451
+ );
383
452
  }
384
453
  render() {
385
454
  while (this.group.firstChild) this.group.removeChild(this.group.firstChild);
386
455
  this.segments = [];
387
- const ep = this.computeEndpoints();
456
+ if (!this.enabled) return;
457
+ const sr = docRect(this.refs.start);
458
+ const er = docRect(this.refs.end);
459
+ if (isDegenerateRect(sr) || isDegenerateRect(er)) {
460
+ this.armReveal();
461
+ return;
462
+ }
463
+ this.teardownReveal();
464
+ const ep = this.computeEndpoints(sr, er);
388
465
  const origin = overlayOrigin(this.svg);
389
466
  const shift = (e) => ({
390
467
  start: { x: e.start.x - origin.x, y: e.start.y - origin.y },
@@ -401,24 +478,29 @@ var ScrollArrow = class {
401
478
  this.seed,
402
479
  this.opts.anchorEnds ?? true
403
480
  );
404
- const obstacles = this.resolveAvoid().map((el) => {
405
- const dr = docRect(el);
406
- return {
407
- left: dr.left - origin.x,
408
- top: dr.top - origin.y,
409
- width: dr.width,
410
- height: dr.height
411
- };
412
- });
413
- const clear = routeOffset(
414
- local.start,
415
- local.end,
416
- obstacles,
417
- this.opts.avoidPadding ?? 14
418
- );
419
- const BOW = 1.6;
420
- const belly = { x: clear.x * BOW, y: clear.y * BOW };
421
- const d = buildPath(local, curvature, belly);
481
+ let d;
482
+ if (this.opts.route === "elbow") {
483
+ d = buildElbowPath(local);
484
+ } else {
485
+ const obstacles = this.resolveAvoid().map((el) => {
486
+ const dr = docRect(el);
487
+ return {
488
+ left: dr.left - origin.x,
489
+ top: dr.top - origin.y,
490
+ width: dr.width,
491
+ height: dr.height
492
+ };
493
+ });
494
+ const clear = routeOffset(
495
+ local.start,
496
+ local.end,
497
+ obstacles,
498
+ this.opts.avoidPadding ?? 14
499
+ );
500
+ const BOW = 1.6;
501
+ const belly = { x: clear.x * BOW, y: clear.y * BOW };
502
+ d = buildPath(local, curvature, belly);
503
+ }
422
504
  this.appendDrawable(this.rc.path(d, roughOpts), "line");
423
505
  const head = this.opts.head;
424
506
  const size = this.opts.headSize;
@@ -529,19 +611,37 @@ var ScrollArrow = class {
529
611
  if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
530
612
  }
531
613
  }
614
+ /**
615
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
616
+ * IntersectionObserver fires when a previously `display:none` element gains a
617
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
618
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
619
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
620
+ */
621
+ armReveal() {
622
+ if (this.revealObs || this.destroyed) return;
623
+ if (typeof IntersectionObserver === "undefined") return;
624
+ this.revealObs = new IntersectionObserver(() => this.render());
625
+ this.revealObs.observe(this.refs.start);
626
+ this.revealObs.observe(this.refs.end);
627
+ }
628
+ teardownReveal() {
629
+ this.revealObs?.disconnect();
630
+ this.revealObs = void 0;
631
+ }
532
632
  bind() {
533
633
  const targets = [this.refs.start, this.refs.end, ...this.resolveAvoid()];
534
634
  if (this.refs.target) targets.push(this.refs.target);
535
635
  this.ro = new ResizeObserver(() => this.render());
536
636
  targets.forEach((t) => this.ro.observe(t));
537
- if (this.opts.scroll !== false) {
637
+ if (this.opts.scroll !== false && !this.reducedMotion) {
538
638
  window.addEventListener("scroll", this.onScroll, true);
539
639
  window.addEventListener("resize", this.onScroll);
540
640
  }
541
641
  }
542
642
  update() {
543
- if (this.destroyed) return;
544
- if (this.opts.scroll === false) {
643
+ if (this.destroyed || !this.enabled) return;
644
+ if (this.opts.scroll === false || this.reducedMotion) {
545
645
  this.applyProgress();
546
646
  return;
547
647
  }
@@ -563,6 +663,140 @@ function clampAt(t) {
563
663
  return t < 0 ? 0 : t > 1 ? 1 : t;
564
664
  }
565
665
 
566
- export { ScrollArrow, easeInOutCubic };
567
- //# sourceMappingURL=chunk-GKWBGFLA.js.map
568
- //# sourceMappingURL=chunk-GKWBGFLA.js.map
666
+ // src/group.ts
667
+ var ScrollArrowGroup = class {
668
+ constructor(options) {
669
+ this.elements = [];
670
+ this.target = null;
671
+ this.progress = 0;
672
+ this.rafId = 0;
673
+ this.destroyed = false;
674
+ this.onScroll = () => {
675
+ if (this.rafId) return;
676
+ this.rafId = requestAnimationFrame(() => {
677
+ this.rafId = 0;
678
+ this.update();
679
+ });
680
+ };
681
+ if (!options.arrows || options.arrows.length === 0) {
682
+ throw new Error("[scroll-arrows] group needs at least one arrow");
683
+ }
684
+ this.opts = {
685
+ stagger: 1,
686
+ speed: 1,
687
+ easing: (t) => t,
688
+ ...options
689
+ };
690
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
691
+ this.enabled = this.opts.enabled ?? true;
692
+ this.arrows = options.arrows.map(
693
+ (a) => new ScrollArrow({
694
+ ...a,
695
+ scroll: false,
696
+ progress: 0,
697
+ respectReducedMotion: false,
698
+ enabled: this.enabled
699
+ })
700
+ );
701
+ this.windows = staggerWindows(this.arrows.length, this.opts.stagger);
702
+ for (const a of options.arrows) {
703
+ const s = resolve2(a.start);
704
+ const e = resolve2(a.end);
705
+ if (s) this.elements.push(s);
706
+ if (e) this.elements.push(e);
707
+ }
708
+ const scroll = this.opts.scroll;
709
+ if (scroll !== false && scroll?.target) {
710
+ this.target = resolve2(scroll.target);
711
+ }
712
+ if (this.reducedMotion) this.progress = 1;
713
+ this.bind();
714
+ this.update();
715
+ }
716
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
717
+ setProgress(p) {
718
+ this.progress = clamp01(p);
719
+ this.distribute();
720
+ }
721
+ /** Recompute geometry for every arrow (call after layout changes). */
722
+ refresh() {
723
+ this.arrows.forEach((a) => a.refresh());
724
+ this.update();
725
+ }
726
+ /**
727
+ * Suspend or restore the whole group without tearing it down. Hides every
728
+ * arrow and stops scroll work when off; restores and recomputes when on.
729
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
730
+ */
731
+ setEnabled(on) {
732
+ if (on === this.enabled || this.destroyed) return;
733
+ this.enabled = on;
734
+ this.arrows.forEach((a) => a.setEnabled(on));
735
+ if (on) {
736
+ this.bind();
737
+ this.update();
738
+ } else {
739
+ window.removeEventListener("scroll", this.onScroll, true);
740
+ window.removeEventListener("resize", this.onScroll);
741
+ cancelAnimationFrame(this.rafId);
742
+ this.rafId = 0;
743
+ }
744
+ }
745
+ destroy() {
746
+ this.destroyed = true;
747
+ window.removeEventListener("scroll", this.onScroll, true);
748
+ window.removeEventListener("resize", this.onScroll);
749
+ cancelAnimationFrame(this.rafId);
750
+ this.arrows.forEach((a) => a.destroy());
751
+ }
752
+ // --- internals ---------------------------------------------------------
753
+ bind() {
754
+ if (this.opts.scroll === false || this.reducedMotion || !this.enabled)
755
+ return;
756
+ window.addEventListener("scroll", this.onScroll, true);
757
+ window.addEventListener("resize", this.onScroll);
758
+ }
759
+ update() {
760
+ if (this.destroyed || !this.enabled) return;
761
+ if (this.opts.scroll === false || this.reducedMotion) {
762
+ this.distribute();
763
+ return;
764
+ }
765
+ const scroll = this.opts.scroll ?? {};
766
+ const range = scroll.range ?? [0.85, 0.35];
767
+ const rect = this.target ? docRect(this.target) : this.groupRect();
768
+ const raw = scrollProgress(rect, range);
769
+ this.progress = clamp01(raw * this.opts.speed);
770
+ this.distribute();
771
+ }
772
+ /** Push each arrow to its sliced local progress for the current group value. */
773
+ distribute() {
774
+ const eased = clamp01(this.opts.easing(this.progress));
775
+ this.arrows.forEach((arrow, i) => {
776
+ arrow.setProgress(windowProgress(eased, this.windows[i]));
777
+ });
778
+ }
779
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
780
+ groupRect() {
781
+ const rects = this.elements.map(docRect);
782
+ if (rects.length === 0) return { left: 0, top: 0, width: 0, height: 0 };
783
+ let left = Infinity;
784
+ let top = Infinity;
785
+ let right = -Infinity;
786
+ let bottom = -Infinity;
787
+ for (const r2 of rects) {
788
+ left = Math.min(left, r2.left);
789
+ top = Math.min(top, r2.top);
790
+ right = Math.max(right, r2.left + r2.width);
791
+ bottom = Math.max(bottom, r2.top + r2.height);
792
+ }
793
+ return { left, top, width: right - left, height: bottom - top };
794
+ }
795
+ };
796
+ function resolve2(ref) {
797
+ return typeof ref === "string" ? document.querySelector(ref) : ref;
798
+ }
799
+
800
+ export { ScrollArrow, ScrollArrowGroup, easeInOutCubic };
801
+ //# sourceMappingURL=chunk-LIT577GH.js.map
802
+ //# sourceMappingURL=chunk-LIT577GH.js.map