scroll-arrows 0.1.3 → 0.3.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as ScrollArrowOptions } from './types-CDd8JqZX.js';
2
- export { A as ArrowHead, E as ElementRef, P as Point, a as ScrollOptions, b as Socket } from './types-CDd8JqZX.js';
1
+ import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-CgHPWybd.js';
2
+ export { A as ArrowHead, E as ElementRef, L as LabelPosition, P as Point, b as ScrollOptions, c as Socket } from './types-CgHPWybd.js';
3
3
 
4
4
  /** A single hand-drawn arrow that draws itself between two elements on scroll. */
5
5
  declare class ScrollArrow {
@@ -18,13 +18,38 @@ declare class ScrollArrow {
18
18
  private segments;
19
19
  /** Representative line stroke + label nodes, when a label is set. */
20
20
  private lineEl;
21
+ /**
22
+ * The smooth ideal path `d` (pre-roughjs). Label placement measures against
23
+ * this, not `lineEl`: rough.js bakes its double stroke into one path with two
24
+ * subpaths, so `lineEl.getTotalLength()` is ~2x the visible curve and would
25
+ * put `labelAt` at twice its intended fraction.
26
+ */
27
+ private lineD;
21
28
  private labelEl;
22
29
  private labelBgEl;
30
+ /** `opts.labelAt` resolved to a 0..1 fraction; cached by renderLabel for the draw loop. */
31
+ private resolvedLabelAt;
23
32
  private progress;
24
33
  private ro?;
34
+ /**
35
+ * Lazily armed only while an anchor is hidden/zero-size, so the common path
36
+ * (both anchors visible) wires no extra observer. Fires render() on reveal.
37
+ */
38
+ private revealObs?;
25
39
  private rafId;
26
40
  private destroyed;
41
+ /** Render static + fully drawn: user prefers reduced motion and we honor it. */
42
+ private reducedMotion;
43
+ /** When false the arrow is hidden and skips all draw/scroll work (no teardown). */
44
+ private enabled;
27
45
  constructor(options: ScrollArrowOptions);
46
+ /**
47
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
48
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
49
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
50
+ * to `matchMedia` to switch arrows off below a breakpoint.
51
+ */
52
+ setEnabled(on: boolean): void;
28
53
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
29
54
  setProgress(p: number): void;
30
55
  /** Recompute geometry (call after layout changes you control). */
@@ -44,14 +69,64 @@ declare class ScrollArrow {
44
69
  * once the line is complete.
45
70
  */
46
71
  private applyProgress;
72
+ /**
73
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
74
+ * IntersectionObserver fires when a previously `display:none` element gains a
75
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
76
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
77
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
78
+ */
79
+ private armReveal;
80
+ private teardownReveal;
81
+ private bind;
82
+ private onScroll;
83
+ private update;
84
+ }
85
+
86
+ /**
87
+ * A coordinated set of arrows that draw in a staggered sequence off one shared
88
+ * scroll trigger (or manual `setProgress`). Owns its arrows; `destroy()` tears
89
+ * them all down.
90
+ */
91
+ declare class ScrollArrowGroup {
92
+ private opts;
93
+ private arrows;
94
+ private windows;
95
+ private elements;
96
+ private target;
97
+ private progress;
98
+ private rafId;
99
+ private destroyed;
100
+ /** Render every arrow static + fully drawn when reduced motion is honored. */
101
+ private reducedMotion;
102
+ /** When false every arrow is hidden and the group does no scroll work. */
103
+ private enabled;
104
+ constructor(options: ScrollArrowGroupOptions);
105
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
106
+ setProgress(p: number): void;
107
+ /** Recompute geometry for every arrow (call after layout changes). */
108
+ refresh(): void;
109
+ /**
110
+ * Suspend or restore the whole group without tearing it down. Hides every
111
+ * arrow and stops scroll work when off; restores and recomputes when on.
112
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
113
+ */
114
+ setEnabled(on: boolean): void;
115
+ destroy(): void;
47
116
  private bind;
48
117
  private onScroll;
49
118
  private update;
119
+ /** Push each arrow to its sliced local progress for the current group value. */
120
+ private distribute;
121
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
122
+ private groupRect;
50
123
  }
51
124
 
52
125
  declare function easeInOutCubic(t: number): number;
53
126
 
54
127
  /** Convenience factory. `const a = scrollArrow({ start, end })`. */
55
128
  declare function scrollArrow(options: ScrollArrowOptions): ScrollArrow;
129
+ /** Convenience factory. `const g = scrollArrowGroup({ arrows: [...] })`. */
130
+ declare function scrollArrowGroup(options: ScrollArrowGroupOptions): ScrollArrowGroup;
56
131
 
57
- export { ScrollArrow, ScrollArrowOptions, easeInOutCubic, scrollArrow };
132
+ export { ScrollArrow, ScrollArrowGroup, ScrollArrowGroupOptions, ScrollArrowOptions, easeInOutCubic, scrollArrow, scrollArrowGroup };
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
- import { ScrollArrow } from './chunk-GKWBGFLA.js';
2
- export { ScrollArrow, easeInOutCubic } from './chunk-GKWBGFLA.js';
1
+ import { ScrollArrow, ScrollArrowGroup } from './chunk-GX722UDR.js';
2
+ export { ScrollArrow, ScrollArrowGroup, easeInOutCubic } from './chunk-GX722UDR.js';
3
3
 
4
4
  // src/index.ts
5
5
  function scrollArrow(options) {
6
6
  return new ScrollArrow(options);
7
7
  }
8
+ function scrollArrowGroup(options) {
9
+ return new ScrollArrowGroup(options);
10
+ }
8
11
 
9
- export { scrollArrow };
12
+ export { scrollArrow, scrollArrowGroup };
10
13
  //# sourceMappingURL=index.js.map
11
14
  //# sourceMappingURL=index.js.map
package/dist/react.cjs CHANGED
@@ -25,20 +25,24 @@ function docRect(el) {
25
25
  height: r2.height
26
26
  };
27
27
  }
28
+ function isDegenerateRect(r2) {
29
+ return r2.width <= 0 || r2.height <= 0;
30
+ }
28
31
  function center(r2) {
29
32
  return { x: r2.left + r2.width / 2, y: r2.top + r2.height / 2 };
30
33
  }
31
- function socketPoint(r2, side) {
34
+ function socketPoint(r2, side, offset = 0) {
32
35
  const c = center(r2);
36
+ const o = offset < -0.5 ? -0.5 : offset > 0.5 ? 0.5 : offset;
33
37
  switch (side) {
34
38
  case "top":
35
- return { x: c.x, y: r2.top };
39
+ return { x: c.x + o * r2.width, y: r2.top };
36
40
  case "bottom":
37
- return { x: c.x, y: r2.top + r2.height };
41
+ return { x: c.x + o * r2.width, y: r2.top + r2.height };
38
42
  case "left":
39
- return { x: r2.left, y: c.y };
43
+ return { x: r2.left, y: c.y + o * r2.height };
40
44
  case "right":
41
- return { x: r2.left + r2.width, y: c.y };
45
+ return { x: r2.left + r2.width, y: c.y + o * r2.height };
42
46
  default:
43
47
  return c;
44
48
  }
@@ -71,12 +75,12 @@ function autoSide(self, other) {
71
75
  }
72
76
  return best;
73
77
  }
74
- function resolveEndpoints(startRect, endRect, startSocket, endSocket) {
78
+ function resolveEndpoints(startRect, endRect, startSocket, endSocket, startOffset = 0, endOffset = 0) {
75
79
  const s = startSocket === "auto" ? autoSide(startRect, endRect) : startSocket;
76
80
  const e = endSocket === "auto" ? autoSide(endRect, startRect) : endSocket;
77
81
  return {
78
- start: socketPoint(startRect, s),
79
- end: socketPoint(endRect, e),
82
+ start: socketPoint(startRect, s, startOffset),
83
+ end: socketPoint(endRect, e, endOffset),
80
84
  startNormal: socketNormal(s),
81
85
  endNormal: socketNormal(e)
82
86
  };
@@ -99,6 +103,26 @@ function buildPath(ep, curvature, belly = { x: 0, y: 0 }) {
99
103
  };
100
104
  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)}`;
101
105
  }
106
+ function buildElbowPath(ep) {
107
+ const { start: s, end: e, startNormal: sn, endNormal: en } = ep;
108
+ const dx = e.x - s.x;
109
+ const dy = e.y - s.y;
110
+ const startVertical = sn.y !== 0 || sn.x === 0 && Math.abs(dy) >= Math.abs(dx);
111
+ const endVertical = en.y !== 0 || en.x === 0 && Math.abs(dy) >= Math.abs(dx);
112
+ let pts;
113
+ if (startVertical && endVertical) {
114
+ const midY = (s.y + e.y) / 2;
115
+ pts = [s, { x: s.x, y: midY }, { x: e.x, y: midY }, e];
116
+ } else if (!startVertical && !endVertical) {
117
+ const midX = (s.x + e.x) / 2;
118
+ pts = [s, { x: midX, y: s.y }, { x: midX, y: e.y }, e];
119
+ } else if (startVertical) {
120
+ pts = [s, { x: s.x, y: e.y }, e];
121
+ } else {
122
+ pts = [s, { x: e.x, y: s.y }, e];
123
+ }
124
+ return `M ${r(pts[0].x)} ${r(pts[0].y)}` + pts.slice(1).map((p) => ` L ${r(p.x)} ${r(p.y)}`).join("");
125
+ }
102
126
  function routeOffset(start, end, obstacles, padding = 14) {
103
127
  const dx = end.x - start.x;
104
128
  const dy = end.y - start.y;
@@ -174,12 +198,29 @@ function easeInOutCubic(t) {
174
198
  function clamp01(t) {
175
199
  return t < 0 ? 0 : t > 1 ? 1 : t;
176
200
  }
201
+ function prefersReducedMotion() {
202
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
203
+ return false;
204
+ }
205
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
206
+ }
177
207
  function scrollProgress(targetRect, range) {
178
208
  const vh = window.innerHeight || 1;
179
209
  const topFrac = (targetRect.top - window.scrollY) / vh;
180
210
  const [enter, leave] = range;
181
211
  return clamp01((enter - topFrac) / (enter - leave || 1));
182
212
  }
213
+ function staggerWindows(n, stagger) {
214
+ if (n <= 0) return [];
215
+ const s = clamp01(stagger);
216
+ const span = 1 / (1 + (n - 1) * s);
217
+ const step = span * s;
218
+ return Array.from({ length: n }, (_, i) => ({ start: i * step, span }));
219
+ }
220
+ function windowProgress(p, w) {
221
+ if (w.span <= 0) return p > w.start ? 1 : 0;
222
+ return clamp01((p - w.start) / w.span);
223
+ }
183
224
  function midpointRect(a, b) {
184
225
  const ra = docRect(a);
185
226
  const rb = docRect(b);
@@ -269,6 +310,25 @@ function clamp012(t) {
269
310
  }
270
311
 
271
312
  // src/draw.ts
313
+ var LABEL_KEYWORDS = {
314
+ start: 0,
315
+ middle: 0.5,
316
+ end: 1
317
+ };
318
+ function resolveLabelAt(value, fallback = 0.5) {
319
+ if (value == null) return fallback;
320
+ if (typeof value === "number") {
321
+ return Number.isFinite(value) ? clamp01(value) : fallback;
322
+ }
323
+ const key = value.trim().toLowerCase();
324
+ const keyword = LABEL_KEYWORDS[key];
325
+ if (keyword !== void 0) return keyword;
326
+ if (key.endsWith("%")) {
327
+ const n = Number.parseFloat(key.slice(0, -1));
328
+ return Number.isFinite(n) ? clamp01(n / 100) : fallback;
329
+ }
330
+ return fallback;
331
+ }
272
332
  function lengths(segs) {
273
333
  let lineLen = 0;
274
334
  let headLen = 0;
@@ -298,7 +358,8 @@ function lineProgress(segs, eased) {
298
358
  return lineLen > 0 ? clamp01(drawn / lineLen) : 1;
299
359
  }
300
360
  function labelOpacity(lineProg, labelAt, fade = 0.08) {
301
- return clamp01((lineProg - clamp01(labelAt)) / (fade || 1));
361
+ const start = Math.min(clamp01(labelAt), 1 - fade);
362
+ return clamp01((lineProg - start) / (fade || 1));
302
363
  }
303
364
 
304
365
  // src/scroll-arrow.ts
@@ -312,8 +373,17 @@ var ScrollArrow = class {
312
373
  this.segments = [];
313
374
  /** Representative line stroke + label nodes, when a label is set. */
314
375
  this.lineEl = null;
376
+ /**
377
+ * The smooth ideal path `d` (pre-roughjs). Label placement measures against
378
+ * this, not `lineEl`: rough.js bakes its double stroke into one path with two
379
+ * subpaths, so `lineEl.getTotalLength()` is ~2x the visible curve and would
380
+ * put `labelAt` at twice its intended fraction.
381
+ */
382
+ this.lineD = "";
315
383
  this.labelEl = null;
316
384
  this.labelBgEl = null;
385
+ /** `opts.labelAt` resolved to a 0..1 fraction; cached by renderLabel for the draw loop. */
386
+ this.resolvedLabelAt = 0.5;
317
387
  this.rafId = 0;
318
388
  this.destroyed = false;
319
389
  this.onScroll = () => {
@@ -337,14 +407,36 @@ var ScrollArrow = class {
337
407
  this.svg = getOverlay(this.container);
338
408
  this.rc = rough__default.default.svg(this.svg);
339
409
  this.svg.appendChild(this.group);
340
- this.progress = clamp01(this.opts.progress);
410
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
411
+ this.progress = this.reducedMotion ? 1 : clamp01(this.opts.progress);
341
412
  this.refs = this.resolveRefs();
342
413
  this.stroke = options.stroke ?? getComputedStyle(this.container).color ?? "#222";
343
414
  this.seed = options.seed ?? deriveSeed(refKey(options.start), refKey(options.end));
415
+ this.enabled = this.opts.enabled ?? true;
416
+ if (!this.enabled) this.group.style.display = "none";
344
417
  this.render();
345
418
  this.bind();
346
419
  this.update();
347
420
  }
421
+ /**
422
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
423
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
424
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
425
+ * to `matchMedia` to switch arrows off below a breakpoint.
426
+ */
427
+ setEnabled(on) {
428
+ if (on === this.enabled || this.destroyed) return;
429
+ this.enabled = on;
430
+ if (on) {
431
+ this.group.style.display = "";
432
+ this.render();
433
+ this.update();
434
+ } else {
435
+ this.group.style.display = "none";
436
+ cancelAnimationFrame(this.rafId);
437
+ this.rafId = 0;
438
+ }
439
+ }
348
440
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
349
441
  setProgress(p) {
350
442
  this.progress = clamp01(p);
@@ -358,6 +450,7 @@ var ScrollArrow = class {
358
450
  destroy() {
359
451
  this.destroyed = true;
360
452
  this.ro?.disconnect();
453
+ this.teardownReveal();
361
454
  window.removeEventListener("scroll", this.onScroll, true);
362
455
  window.removeEventListener("resize", this.onScroll);
363
456
  cancelAnimationFrame(this.rafId);
@@ -383,17 +476,30 @@ var ScrollArrow = class {
383
476
  const list = Array.isArray(a) ? a : [a];
384
477
  return list.map(resolve).filter((el) => el !== null);
385
478
  }
386
- computeEndpoints() {
387
- const sr = docRect(this.refs.start);
388
- const er = docRect(this.refs.end);
479
+ computeEndpoints(sr, er) {
389
480
  const ss = this.opts.startSocket ?? "auto";
390
481
  const es = this.opts.endSocket ?? "auto";
391
- return resolveEndpoints(sr, er, ss, es);
482
+ return resolveEndpoints(
483
+ sr,
484
+ er,
485
+ ss,
486
+ es,
487
+ this.opts.startSocketOffset ?? 0,
488
+ this.opts.endSocketOffset ?? 0
489
+ );
392
490
  }
393
491
  render() {
394
492
  while (this.group.firstChild) this.group.removeChild(this.group.firstChild);
395
493
  this.segments = [];
396
- const ep = this.computeEndpoints();
494
+ if (!this.enabled) return;
495
+ const sr = docRect(this.refs.start);
496
+ const er = docRect(this.refs.end);
497
+ if (isDegenerateRect(sr) || isDegenerateRect(er)) {
498
+ this.armReveal();
499
+ return;
500
+ }
501
+ this.teardownReveal();
502
+ const ep = this.computeEndpoints(sr, er);
397
503
  const origin = overlayOrigin(this.svg);
398
504
  const shift = (e) => ({
399
505
  start: { x: e.start.x - origin.x, y: e.start.y - origin.y },
@@ -410,24 +516,30 @@ var ScrollArrow = class {
410
516
  this.seed,
411
517
  this.opts.anchorEnds ?? true
412
518
  );
413
- const obstacles = this.resolveAvoid().map((el) => {
414
- const dr = docRect(el);
415
- return {
416
- left: dr.left - origin.x,
417
- top: dr.top - origin.y,
418
- width: dr.width,
419
- height: dr.height
420
- };
421
- });
422
- const clear = routeOffset(
423
- local.start,
424
- local.end,
425
- obstacles,
426
- this.opts.avoidPadding ?? 14
427
- );
428
- const BOW = 1.6;
429
- const belly = { x: clear.x * BOW, y: clear.y * BOW };
430
- const d = buildPath(local, curvature, belly);
519
+ let d;
520
+ if (this.opts.route === "elbow") {
521
+ d = buildElbowPath(local);
522
+ } else {
523
+ const obstacles = this.resolveAvoid().map((el) => {
524
+ const dr = docRect(el);
525
+ return {
526
+ left: dr.left - origin.x,
527
+ top: dr.top - origin.y,
528
+ width: dr.width,
529
+ height: dr.height
530
+ };
531
+ });
532
+ const clear = routeOffset(
533
+ local.start,
534
+ local.end,
535
+ obstacles,
536
+ this.opts.avoidPadding ?? 14
537
+ );
538
+ const BOW = 1.6;
539
+ const belly = { x: clear.x * BOW, y: clear.y * BOW };
540
+ d = buildPath(local, curvature, belly);
541
+ }
542
+ this.lineD = d;
431
543
  this.appendDrawable(this.rc.path(d, roughOpts), "line");
432
544
  const head = this.opts.head;
433
545
  const size = this.opts.headSize;
@@ -464,25 +576,26 @@ var ScrollArrow = class {
464
576
  this.labelEl = null;
465
577
  this.labelBgEl = null;
466
578
  const text = this.opts.label;
467
- if (!text || !this.lineEl) return;
468
- const total = this.lineEl.getTotalLength();
469
- const at = clampAt(this.opts.labelAt ?? 0.5);
470
- const pt = this.lineEl.getPointAtLength(at * total);
579
+ if (!text || !this.lineEl || !this.lineD) return;
580
+ const at = resolveLabelAt(this.opts.labelAt);
581
+ this.resolvedLabelAt = at;
582
+ const measure = createSvgEl("path");
583
+ measure.setAttribute("d", this.lineD);
584
+ this.group.appendChild(measure);
585
+ const total = measure.getTotalLength();
586
+ const pt = measure.getPointAtLength(at * total);
471
587
  const offset = this.opts.labelOffset ?? 0;
472
588
  let x = pt.x;
473
589
  let y = pt.y;
474
590
  if (offset && total > 0) {
475
591
  const eps = Math.min(1, total / 2);
476
- const before = this.lineEl.getPointAtLength(
477
- Math.max(0, at * total - eps)
478
- );
479
- const after = this.lineEl.getPointAtLength(
480
- Math.min(total, at * total + eps)
481
- );
592
+ const before = measure.getPointAtLength(Math.max(0, at * total - eps));
593
+ const after = measure.getPointAtLength(Math.min(total, at * total + eps));
482
594
  const n = unitNormal(before, after);
483
595
  x += n.x * offset;
484
596
  y += n.y * offset;
485
597
  }
598
+ this.group.removeChild(measure);
486
599
  const label = createSvgEl("text");
487
600
  label.textContent = text;
488
601
  label.setAttribute("x", String(x));
@@ -532,25 +645,43 @@ var ScrollArrow = class {
532
645
  if (this.labelEl) {
533
646
  const op = labelOpacity(
534
647
  lineProgress(this.segments, eased),
535
- this.opts.labelAt ?? 0.5
648
+ this.resolvedLabelAt
536
649
  );
537
650
  this.labelEl.style.opacity = String(op);
538
651
  if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
539
652
  }
540
653
  }
654
+ /**
655
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
656
+ * IntersectionObserver fires when a previously `display:none` element gains a
657
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
658
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
659
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
660
+ */
661
+ armReveal() {
662
+ if (this.revealObs || this.destroyed) return;
663
+ if (typeof IntersectionObserver === "undefined") return;
664
+ this.revealObs = new IntersectionObserver(() => this.render());
665
+ this.revealObs.observe(this.refs.start);
666
+ this.revealObs.observe(this.refs.end);
667
+ }
668
+ teardownReveal() {
669
+ this.revealObs?.disconnect();
670
+ this.revealObs = void 0;
671
+ }
541
672
  bind() {
542
673
  const targets = [this.refs.start, this.refs.end, ...this.resolveAvoid()];
543
674
  if (this.refs.target) targets.push(this.refs.target);
544
675
  this.ro = new ResizeObserver(() => this.render());
545
676
  targets.forEach((t) => this.ro.observe(t));
546
- if (this.opts.scroll !== false) {
677
+ if (this.opts.scroll !== false && !this.reducedMotion) {
547
678
  window.addEventListener("scroll", this.onScroll, true);
548
679
  window.addEventListener("resize", this.onScroll);
549
680
  }
550
681
  }
551
682
  update() {
552
- if (this.destroyed) return;
553
- if (this.opts.scroll === false) {
683
+ if (this.destroyed || !this.enabled) return;
684
+ if (this.opts.scroll === false || this.reducedMotion) {
554
685
  this.applyProgress();
555
686
  return;
556
687
  }
@@ -568,8 +699,139 @@ function resolve(ref) {
568
699
  function refKey(ref) {
569
700
  return typeof ref === "string" ? ref : ref.tagName + (ref.id ? "#" + ref.id : "");
570
701
  }
571
- function clampAt(t) {
572
- return t < 0 ? 0 : t > 1 ? 1 : t;
702
+
703
+ // src/group.ts
704
+ var ScrollArrowGroup = class {
705
+ constructor(options) {
706
+ this.elements = [];
707
+ this.target = null;
708
+ this.progress = 0;
709
+ this.rafId = 0;
710
+ this.destroyed = false;
711
+ this.onScroll = () => {
712
+ if (this.rafId) return;
713
+ this.rafId = requestAnimationFrame(() => {
714
+ this.rafId = 0;
715
+ this.update();
716
+ });
717
+ };
718
+ if (!options.arrows || options.arrows.length === 0) {
719
+ throw new Error("[scroll-arrows] group needs at least one arrow");
720
+ }
721
+ this.opts = {
722
+ stagger: 1,
723
+ speed: 1,
724
+ easing: (t) => t,
725
+ ...options
726
+ };
727
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
728
+ this.enabled = this.opts.enabled ?? true;
729
+ this.arrows = options.arrows.map(
730
+ (a) => new ScrollArrow({
731
+ ...a,
732
+ scroll: false,
733
+ progress: 0,
734
+ respectReducedMotion: false,
735
+ enabled: this.enabled
736
+ })
737
+ );
738
+ this.windows = staggerWindows(this.arrows.length, this.opts.stagger);
739
+ for (const a of options.arrows) {
740
+ const s = resolve2(a.start);
741
+ const e = resolve2(a.end);
742
+ if (s) this.elements.push(s);
743
+ if (e) this.elements.push(e);
744
+ }
745
+ const scroll = this.opts.scroll;
746
+ if (scroll !== false && scroll?.target) {
747
+ this.target = resolve2(scroll.target);
748
+ }
749
+ if (this.reducedMotion) this.progress = 1;
750
+ this.bind();
751
+ this.update();
752
+ }
753
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
754
+ setProgress(p) {
755
+ this.progress = clamp01(p);
756
+ this.distribute();
757
+ }
758
+ /** Recompute geometry for every arrow (call after layout changes). */
759
+ refresh() {
760
+ this.arrows.forEach((a) => a.refresh());
761
+ this.update();
762
+ }
763
+ /**
764
+ * Suspend or restore the whole group without tearing it down. Hides every
765
+ * arrow and stops scroll work when off; restores and recomputes when on.
766
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
767
+ */
768
+ setEnabled(on) {
769
+ if (on === this.enabled || this.destroyed) return;
770
+ this.enabled = on;
771
+ this.arrows.forEach((a) => a.setEnabled(on));
772
+ if (on) {
773
+ this.bind();
774
+ this.update();
775
+ } else {
776
+ window.removeEventListener("scroll", this.onScroll, true);
777
+ window.removeEventListener("resize", this.onScroll);
778
+ cancelAnimationFrame(this.rafId);
779
+ this.rafId = 0;
780
+ }
781
+ }
782
+ destroy() {
783
+ this.destroyed = true;
784
+ window.removeEventListener("scroll", this.onScroll, true);
785
+ window.removeEventListener("resize", this.onScroll);
786
+ cancelAnimationFrame(this.rafId);
787
+ this.arrows.forEach((a) => a.destroy());
788
+ }
789
+ // --- internals ---------------------------------------------------------
790
+ bind() {
791
+ if (this.opts.scroll === false || this.reducedMotion || !this.enabled)
792
+ return;
793
+ window.addEventListener("scroll", this.onScroll, true);
794
+ window.addEventListener("resize", this.onScroll);
795
+ }
796
+ update() {
797
+ if (this.destroyed || !this.enabled) return;
798
+ if (this.opts.scroll === false || this.reducedMotion) {
799
+ this.distribute();
800
+ return;
801
+ }
802
+ const scroll = this.opts.scroll ?? {};
803
+ const range = scroll.range ?? [0.85, 0.35];
804
+ const rect = this.target ? docRect(this.target) : this.groupRect();
805
+ const raw = scrollProgress(rect, range);
806
+ this.progress = clamp01(raw * this.opts.speed);
807
+ this.distribute();
808
+ }
809
+ /** Push each arrow to its sliced local progress for the current group value. */
810
+ distribute() {
811
+ const eased = clamp01(this.opts.easing(this.progress));
812
+ this.arrows.forEach((arrow, i) => {
813
+ arrow.setProgress(windowProgress(eased, this.windows[i]));
814
+ });
815
+ }
816
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
817
+ groupRect() {
818
+ const rects = this.elements.map(docRect);
819
+ if (rects.length === 0) return { left: 0, top: 0, width: 0, height: 0 };
820
+ let left = Infinity;
821
+ let top = Infinity;
822
+ let right = -Infinity;
823
+ let bottom = -Infinity;
824
+ for (const r2 of rects) {
825
+ left = Math.min(left, r2.left);
826
+ top = Math.min(top, r2.top);
827
+ right = Math.max(right, r2.left + r2.width);
828
+ bottom = Math.max(bottom, r2.top + r2.height);
829
+ }
830
+ return { left, top, width: right - left, height: bottom - top };
831
+ }
832
+ };
833
+ function resolve2(ref) {
834
+ return typeof ref === "string" ? document.querySelector(ref) : ref;
573
835
  }
574
836
 
575
837
  // src/react.tsx
@@ -597,8 +859,33 @@ function ScrollArrowLine(props) {
597
859
  useScrollArrow(props);
598
860
  return null;
599
861
  }
862
+ function useScrollArrowGroup(options) {
863
+ const groupRef = react.useRef(null);
864
+ const { arrows, deps = [], ...rest } = options;
865
+ react.useEffect(() => {
866
+ const resolved = arrows.map((a) => {
867
+ const s = read(a.start);
868
+ const e = read(a.end);
869
+ if (!s || !e) return null;
870
+ return { ...a, start: s, end: e };
871
+ }).filter((a) => a !== null);
872
+ if (resolved.length === 0) return;
873
+ const group = new ScrollArrowGroup({ ...rest, arrows: resolved });
874
+ groupRef.current = group;
875
+ return () => {
876
+ group.destroy();
877
+ groupRef.current = null;
878
+ };
879
+ }, deps);
880
+ }
881
+ function ScrollArrowGroupLines(props) {
882
+ useScrollArrowGroup(props);
883
+ return null;
884
+ }
600
885
 
886
+ exports.ScrollArrowGroupLines = ScrollArrowGroupLines;
601
887
  exports.ScrollArrowLine = ScrollArrowLine;
602
888
  exports.useScrollArrow = useScrollArrow;
889
+ exports.useScrollArrowGroup = useScrollArrowGroup;
603
890
  //# sourceMappingURL=react.cjs.map
604
891
  //# sourceMappingURL=react.cjs.map