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/dist/index.cjs CHANGED
@@ -24,20 +24,24 @@ function docRect(el) {
24
24
  height: r2.height
25
25
  };
26
26
  }
27
+ function isDegenerateRect(r2) {
28
+ return r2.width <= 0 || r2.height <= 0;
29
+ }
27
30
  function center(r2) {
28
31
  return { x: r2.left + r2.width / 2, y: r2.top + r2.height / 2 };
29
32
  }
30
- function socketPoint(r2, side) {
33
+ function socketPoint(r2, side, offset = 0) {
31
34
  const c = center(r2);
35
+ const o = offset < -0.5 ? -0.5 : offset > 0.5 ? 0.5 : offset;
32
36
  switch (side) {
33
37
  case "top":
34
- return { x: c.x, y: r2.top };
38
+ return { x: c.x + o * r2.width, y: r2.top };
35
39
  case "bottom":
36
- return { x: c.x, y: r2.top + r2.height };
40
+ return { x: c.x + o * r2.width, y: r2.top + r2.height };
37
41
  case "left":
38
- return { x: r2.left, y: c.y };
42
+ return { x: r2.left, y: c.y + o * r2.height };
39
43
  case "right":
40
- return { x: r2.left + r2.width, y: c.y };
44
+ return { x: r2.left + r2.width, y: c.y + o * r2.height };
41
45
  default:
42
46
  return c;
43
47
  }
@@ -70,12 +74,12 @@ function autoSide(self, other) {
70
74
  }
71
75
  return best;
72
76
  }
73
- function resolveEndpoints(startRect, endRect, startSocket, endSocket) {
77
+ function resolveEndpoints(startRect, endRect, startSocket, endSocket, startOffset = 0, endOffset = 0) {
74
78
  const s = startSocket === "auto" ? autoSide(startRect, endRect) : startSocket;
75
79
  const e = endSocket === "auto" ? autoSide(endRect, startRect) : endSocket;
76
80
  return {
77
- start: socketPoint(startRect, s),
78
- end: socketPoint(endRect, e),
81
+ start: socketPoint(startRect, s, startOffset),
82
+ end: socketPoint(endRect, e, endOffset),
79
83
  startNormal: socketNormal(s),
80
84
  endNormal: socketNormal(e)
81
85
  };
@@ -98,6 +102,26 @@ function buildPath(ep, curvature, belly = { x: 0, y: 0 }) {
98
102
  };
99
103
  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)}`;
100
104
  }
105
+ function buildElbowPath(ep) {
106
+ const { start: s, end: e, startNormal: sn, endNormal: en } = ep;
107
+ const dx = e.x - s.x;
108
+ const dy = e.y - s.y;
109
+ const startVertical = sn.y !== 0 || sn.x === 0 && Math.abs(dy) >= Math.abs(dx);
110
+ const endVertical = en.y !== 0 || en.x === 0 && Math.abs(dy) >= Math.abs(dx);
111
+ let pts;
112
+ if (startVertical && endVertical) {
113
+ const midY = (s.y + e.y) / 2;
114
+ pts = [s, { x: s.x, y: midY }, { x: e.x, y: midY }, e];
115
+ } else if (!startVertical && !endVertical) {
116
+ const midX = (s.x + e.x) / 2;
117
+ pts = [s, { x: midX, y: s.y }, { x: midX, y: e.y }, e];
118
+ } else if (startVertical) {
119
+ pts = [s, { x: s.x, y: e.y }, e];
120
+ } else {
121
+ pts = [s, { x: e.x, y: s.y }, e];
122
+ }
123
+ return `M ${r(pts[0].x)} ${r(pts[0].y)}` + pts.slice(1).map((p) => ` L ${r(p.x)} ${r(p.y)}`).join("");
124
+ }
101
125
  function routeOffset(start, end, obstacles, padding = 14) {
102
126
  const dx = end.x - start.x;
103
127
  const dy = end.y - start.y;
@@ -173,12 +197,29 @@ function easeInOutCubic(t) {
173
197
  function clamp01(t) {
174
198
  return t < 0 ? 0 : t > 1 ? 1 : t;
175
199
  }
200
+ function prefersReducedMotion() {
201
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
202
+ return false;
203
+ }
204
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
205
+ }
176
206
  function scrollProgress(targetRect, range) {
177
207
  const vh = window.innerHeight || 1;
178
208
  const topFrac = (targetRect.top - window.scrollY) / vh;
179
209
  const [enter, leave] = range;
180
210
  return clamp01((enter - topFrac) / (enter - leave || 1));
181
211
  }
212
+ function staggerWindows(n, stagger) {
213
+ if (n <= 0) return [];
214
+ const s = clamp01(stagger);
215
+ const span = 1 / (1 + (n - 1) * s);
216
+ const step = span * s;
217
+ return Array.from({ length: n }, (_, i) => ({ start: i * step, span }));
218
+ }
219
+ function windowProgress(p, w) {
220
+ if (w.span <= 0) return p > w.start ? 1 : 0;
221
+ return clamp01((p - w.start) / w.span);
222
+ }
182
223
  function midpointRect(a, b) {
183
224
  const ra = docRect(a);
184
225
  const rb = docRect(b);
@@ -336,14 +377,36 @@ var ScrollArrow = class {
336
377
  this.svg = getOverlay(this.container);
337
378
  this.rc = rough__default.default.svg(this.svg);
338
379
  this.svg.appendChild(this.group);
339
- this.progress = clamp01(this.opts.progress);
380
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
381
+ this.progress = this.reducedMotion ? 1 : clamp01(this.opts.progress);
340
382
  this.refs = this.resolveRefs();
341
383
  this.stroke = options.stroke ?? getComputedStyle(this.container).color ?? "#222";
342
384
  this.seed = options.seed ?? deriveSeed(refKey(options.start), refKey(options.end));
385
+ this.enabled = this.opts.enabled ?? true;
386
+ if (!this.enabled) this.group.style.display = "none";
343
387
  this.render();
344
388
  this.bind();
345
389
  this.update();
346
390
  }
391
+ /**
392
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
393
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
394
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
395
+ * to `matchMedia` to switch arrows off below a breakpoint.
396
+ */
397
+ setEnabled(on) {
398
+ if (on === this.enabled || this.destroyed) return;
399
+ this.enabled = on;
400
+ if (on) {
401
+ this.group.style.display = "";
402
+ this.render();
403
+ this.update();
404
+ } else {
405
+ this.group.style.display = "none";
406
+ cancelAnimationFrame(this.rafId);
407
+ this.rafId = 0;
408
+ }
409
+ }
347
410
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
348
411
  setProgress(p) {
349
412
  this.progress = clamp01(p);
@@ -357,6 +420,7 @@ var ScrollArrow = class {
357
420
  destroy() {
358
421
  this.destroyed = true;
359
422
  this.ro?.disconnect();
423
+ this.teardownReveal();
360
424
  window.removeEventListener("scroll", this.onScroll, true);
361
425
  window.removeEventListener("resize", this.onScroll);
362
426
  cancelAnimationFrame(this.rafId);
@@ -382,17 +446,30 @@ var ScrollArrow = class {
382
446
  const list = Array.isArray(a) ? a : [a];
383
447
  return list.map(resolve).filter((el) => el !== null);
384
448
  }
385
- computeEndpoints() {
386
- const sr = docRect(this.refs.start);
387
- const er = docRect(this.refs.end);
449
+ computeEndpoints(sr, er) {
388
450
  const ss = this.opts.startSocket ?? "auto";
389
451
  const es = this.opts.endSocket ?? "auto";
390
- return resolveEndpoints(sr, er, ss, es);
452
+ return resolveEndpoints(
453
+ sr,
454
+ er,
455
+ ss,
456
+ es,
457
+ this.opts.startSocketOffset ?? 0,
458
+ this.opts.endSocketOffset ?? 0
459
+ );
391
460
  }
392
461
  render() {
393
462
  while (this.group.firstChild) this.group.removeChild(this.group.firstChild);
394
463
  this.segments = [];
395
- const ep = this.computeEndpoints();
464
+ if (!this.enabled) return;
465
+ const sr = docRect(this.refs.start);
466
+ const er = docRect(this.refs.end);
467
+ if (isDegenerateRect(sr) || isDegenerateRect(er)) {
468
+ this.armReveal();
469
+ return;
470
+ }
471
+ this.teardownReveal();
472
+ const ep = this.computeEndpoints(sr, er);
396
473
  const origin = overlayOrigin(this.svg);
397
474
  const shift = (e) => ({
398
475
  start: { x: e.start.x - origin.x, y: e.start.y - origin.y },
@@ -409,24 +486,29 @@ var ScrollArrow = class {
409
486
  this.seed,
410
487
  this.opts.anchorEnds ?? true
411
488
  );
412
- const obstacles = this.resolveAvoid().map((el) => {
413
- const dr = docRect(el);
414
- return {
415
- left: dr.left - origin.x,
416
- top: dr.top - origin.y,
417
- width: dr.width,
418
- height: dr.height
419
- };
420
- });
421
- const clear = routeOffset(
422
- local.start,
423
- local.end,
424
- obstacles,
425
- this.opts.avoidPadding ?? 14
426
- );
427
- const BOW = 1.6;
428
- const belly = { x: clear.x * BOW, y: clear.y * BOW };
429
- const d = buildPath(local, curvature, belly);
489
+ let d;
490
+ if (this.opts.route === "elbow") {
491
+ d = buildElbowPath(local);
492
+ } else {
493
+ const obstacles = this.resolveAvoid().map((el) => {
494
+ const dr = docRect(el);
495
+ return {
496
+ left: dr.left - origin.x,
497
+ top: dr.top - origin.y,
498
+ width: dr.width,
499
+ height: dr.height
500
+ };
501
+ });
502
+ const clear = routeOffset(
503
+ local.start,
504
+ local.end,
505
+ obstacles,
506
+ this.opts.avoidPadding ?? 14
507
+ );
508
+ const BOW = 1.6;
509
+ const belly = { x: clear.x * BOW, y: clear.y * BOW };
510
+ d = buildPath(local, curvature, belly);
511
+ }
430
512
  this.appendDrawable(this.rc.path(d, roughOpts), "line");
431
513
  const head = this.opts.head;
432
514
  const size = this.opts.headSize;
@@ -537,19 +619,37 @@ var ScrollArrow = class {
537
619
  if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
538
620
  }
539
621
  }
622
+ /**
623
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
624
+ * IntersectionObserver fires when a previously `display:none` element gains a
625
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
626
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
627
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
628
+ */
629
+ armReveal() {
630
+ if (this.revealObs || this.destroyed) return;
631
+ if (typeof IntersectionObserver === "undefined") return;
632
+ this.revealObs = new IntersectionObserver(() => this.render());
633
+ this.revealObs.observe(this.refs.start);
634
+ this.revealObs.observe(this.refs.end);
635
+ }
636
+ teardownReveal() {
637
+ this.revealObs?.disconnect();
638
+ this.revealObs = void 0;
639
+ }
540
640
  bind() {
541
641
  const targets = [this.refs.start, this.refs.end, ...this.resolveAvoid()];
542
642
  if (this.refs.target) targets.push(this.refs.target);
543
643
  this.ro = new ResizeObserver(() => this.render());
544
644
  targets.forEach((t) => this.ro.observe(t));
545
- if (this.opts.scroll !== false) {
645
+ if (this.opts.scroll !== false && !this.reducedMotion) {
546
646
  window.addEventListener("scroll", this.onScroll, true);
547
647
  window.addEventListener("resize", this.onScroll);
548
648
  }
549
649
  }
550
650
  update() {
551
- if (this.destroyed) return;
552
- if (this.opts.scroll === false) {
651
+ if (this.destroyed || !this.enabled) return;
652
+ if (this.opts.scroll === false || this.reducedMotion) {
553
653
  this.applyProgress();
554
654
  return;
555
655
  }
@@ -571,13 +671,152 @@ function clampAt(t) {
571
671
  return t < 0 ? 0 : t > 1 ? 1 : t;
572
672
  }
573
673
 
674
+ // src/group.ts
675
+ var ScrollArrowGroup = class {
676
+ constructor(options) {
677
+ this.elements = [];
678
+ this.target = null;
679
+ this.progress = 0;
680
+ this.rafId = 0;
681
+ this.destroyed = false;
682
+ this.onScroll = () => {
683
+ if (this.rafId) return;
684
+ this.rafId = requestAnimationFrame(() => {
685
+ this.rafId = 0;
686
+ this.update();
687
+ });
688
+ };
689
+ if (!options.arrows || options.arrows.length === 0) {
690
+ throw new Error("[scroll-arrows] group needs at least one arrow");
691
+ }
692
+ this.opts = {
693
+ stagger: 1,
694
+ speed: 1,
695
+ easing: (t) => t,
696
+ ...options
697
+ };
698
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
699
+ this.enabled = this.opts.enabled ?? true;
700
+ this.arrows = options.arrows.map(
701
+ (a) => new ScrollArrow({
702
+ ...a,
703
+ scroll: false,
704
+ progress: 0,
705
+ respectReducedMotion: false,
706
+ enabled: this.enabled
707
+ })
708
+ );
709
+ this.windows = staggerWindows(this.arrows.length, this.opts.stagger);
710
+ for (const a of options.arrows) {
711
+ const s = resolve2(a.start);
712
+ const e = resolve2(a.end);
713
+ if (s) this.elements.push(s);
714
+ if (e) this.elements.push(e);
715
+ }
716
+ const scroll = this.opts.scroll;
717
+ if (scroll !== false && scroll?.target) {
718
+ this.target = resolve2(scroll.target);
719
+ }
720
+ if (this.reducedMotion) this.progress = 1;
721
+ this.bind();
722
+ this.update();
723
+ }
724
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
725
+ setProgress(p) {
726
+ this.progress = clamp01(p);
727
+ this.distribute();
728
+ }
729
+ /** Recompute geometry for every arrow (call after layout changes). */
730
+ refresh() {
731
+ this.arrows.forEach((a) => a.refresh());
732
+ this.update();
733
+ }
734
+ /**
735
+ * Suspend or restore the whole group without tearing it down. Hides every
736
+ * arrow and stops scroll work when off; restores and recomputes when on.
737
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
738
+ */
739
+ setEnabled(on) {
740
+ if (on === this.enabled || this.destroyed) return;
741
+ this.enabled = on;
742
+ this.arrows.forEach((a) => a.setEnabled(on));
743
+ if (on) {
744
+ this.bind();
745
+ this.update();
746
+ } else {
747
+ window.removeEventListener("scroll", this.onScroll, true);
748
+ window.removeEventListener("resize", this.onScroll);
749
+ cancelAnimationFrame(this.rafId);
750
+ this.rafId = 0;
751
+ }
752
+ }
753
+ destroy() {
754
+ this.destroyed = true;
755
+ window.removeEventListener("scroll", this.onScroll, true);
756
+ window.removeEventListener("resize", this.onScroll);
757
+ cancelAnimationFrame(this.rafId);
758
+ this.arrows.forEach((a) => a.destroy());
759
+ }
760
+ // --- internals ---------------------------------------------------------
761
+ bind() {
762
+ if (this.opts.scroll === false || this.reducedMotion || !this.enabled)
763
+ return;
764
+ window.addEventListener("scroll", this.onScroll, true);
765
+ window.addEventListener("resize", this.onScroll);
766
+ }
767
+ update() {
768
+ if (this.destroyed || !this.enabled) return;
769
+ if (this.opts.scroll === false || this.reducedMotion) {
770
+ this.distribute();
771
+ return;
772
+ }
773
+ const scroll = this.opts.scroll ?? {};
774
+ const range = scroll.range ?? [0.85, 0.35];
775
+ const rect = this.target ? docRect(this.target) : this.groupRect();
776
+ const raw = scrollProgress(rect, range);
777
+ this.progress = clamp01(raw * this.opts.speed);
778
+ this.distribute();
779
+ }
780
+ /** Push each arrow to its sliced local progress for the current group value. */
781
+ distribute() {
782
+ const eased = clamp01(this.opts.easing(this.progress));
783
+ this.arrows.forEach((arrow, i) => {
784
+ arrow.setProgress(windowProgress(eased, this.windows[i]));
785
+ });
786
+ }
787
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
788
+ groupRect() {
789
+ const rects = this.elements.map(docRect);
790
+ if (rects.length === 0) return { left: 0, top: 0, width: 0, height: 0 };
791
+ let left = Infinity;
792
+ let top = Infinity;
793
+ let right = -Infinity;
794
+ let bottom = -Infinity;
795
+ for (const r2 of rects) {
796
+ left = Math.min(left, r2.left);
797
+ top = Math.min(top, r2.top);
798
+ right = Math.max(right, r2.left + r2.width);
799
+ bottom = Math.max(bottom, r2.top + r2.height);
800
+ }
801
+ return { left, top, width: right - left, height: bottom - top };
802
+ }
803
+ };
804
+ function resolve2(ref) {
805
+ return typeof ref === "string" ? document.querySelector(ref) : ref;
806
+ }
807
+
574
808
  // src/index.ts
575
809
  function scrollArrow(options) {
576
810
  return new ScrollArrow(options);
577
811
  }
812
+ function scrollArrowGroup(options) {
813
+ return new ScrollArrowGroup(options);
814
+ }
578
815
 
579
816
  exports.ScrollArrow = ScrollArrow;
817
+ exports.ScrollArrowGroup = ScrollArrowGroup;
580
818
  exports.easeInOutCubic = easeInOutCubic;
581
819
  exports.scrollArrow = scrollArrow;
820
+ exports.scrollArrowGroup = scrollArrowGroup;
582
821
  //# sourceMappingURL=index.cjs.map
583
822
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as ScrollArrowOptions } from './types-CDd8JqZX.cjs';
2
- export { A as ArrowHead, E as ElementRef, P as Point, a as ScrollOptions, b as Socket } from './types-CDd8JqZX.cjs';
1
+ import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-Cpvz9wtr.cjs';
2
+ export { A as ArrowHead, E as ElementRef, P as Point, b as ScrollOptions, c as Socket } from './types-Cpvz9wtr.cjs';
3
3
 
4
4
  /** A single hand-drawn arrow that draws itself between two elements on scroll. */
5
5
  declare class ScrollArrow {
@@ -22,9 +22,25 @@ declare class ScrollArrow {
22
22
  private labelBgEl;
23
23
  private progress;
24
24
  private ro?;
25
+ /**
26
+ * Lazily armed only while an anchor is hidden/zero-size, so the common path
27
+ * (both anchors visible) wires no extra observer. Fires render() on reveal.
28
+ */
29
+ private revealObs?;
25
30
  private rafId;
26
31
  private destroyed;
32
+ /** Render static + fully drawn: user prefers reduced motion and we honor it. */
33
+ private reducedMotion;
34
+ /** When false the arrow is hidden and skips all draw/scroll work (no teardown). */
35
+ private enabled;
27
36
  constructor(options: ScrollArrowOptions);
37
+ /**
38
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
39
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
40
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
41
+ * to `matchMedia` to switch arrows off below a breakpoint.
42
+ */
43
+ setEnabled(on: boolean): void;
28
44
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
29
45
  setProgress(p: number): void;
30
46
  /** Recompute geometry (call after layout changes you control). */
@@ -44,14 +60,64 @@ declare class ScrollArrow {
44
60
  * once the line is complete.
45
61
  */
46
62
  private applyProgress;
63
+ /**
64
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
65
+ * IntersectionObserver fires when a previously `display:none` element gains a
66
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
67
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
68
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
69
+ */
70
+ private armReveal;
71
+ private teardownReveal;
72
+ private bind;
73
+ private onScroll;
74
+ private update;
75
+ }
76
+
77
+ /**
78
+ * A coordinated set of arrows that draw in a staggered sequence off one shared
79
+ * scroll trigger (or manual `setProgress`). Owns its arrows; `destroy()` tears
80
+ * them all down.
81
+ */
82
+ declare class ScrollArrowGroup {
83
+ private opts;
84
+ private arrows;
85
+ private windows;
86
+ private elements;
87
+ private target;
88
+ private progress;
89
+ private rafId;
90
+ private destroyed;
91
+ /** Render every arrow static + fully drawn when reduced motion is honored. */
92
+ private reducedMotion;
93
+ /** When false every arrow is hidden and the group does no scroll work. */
94
+ private enabled;
95
+ constructor(options: ScrollArrowGroupOptions);
96
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
97
+ setProgress(p: number): void;
98
+ /** Recompute geometry for every arrow (call after layout changes). */
99
+ refresh(): void;
100
+ /**
101
+ * Suspend or restore the whole group without tearing it down. Hides every
102
+ * arrow and stops scroll work when off; restores and recomputes when on.
103
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
104
+ */
105
+ setEnabled(on: boolean): void;
106
+ destroy(): void;
47
107
  private bind;
48
108
  private onScroll;
49
109
  private update;
110
+ /** Push each arrow to its sliced local progress for the current group value. */
111
+ private distribute;
112
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
113
+ private groupRect;
50
114
  }
51
115
 
52
116
  declare function easeInOutCubic(t: number): number;
53
117
 
54
118
  /** Convenience factory. `const a = scrollArrow({ start, end })`. */
55
119
  declare function scrollArrow(options: ScrollArrowOptions): ScrollArrow;
120
+ /** Convenience factory. `const g = scrollArrowGroup({ arrows: [...] })`. */
121
+ declare function scrollArrowGroup(options: ScrollArrowGroupOptions): ScrollArrowGroup;
56
122
 
57
- export { ScrollArrow, ScrollArrowOptions, easeInOutCubic, scrollArrow };
123
+ export { ScrollArrow, ScrollArrowGroup, ScrollArrowGroupOptions, ScrollArrowOptions, easeInOutCubic, scrollArrow, scrollArrowGroup };
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-Cpvz9wtr.js';
2
+ export { A as ArrowHead, E as ElementRef, P as Point, b as ScrollOptions, c as Socket } from './types-Cpvz9wtr.js';
3
3
 
4
4
  /** A single hand-drawn arrow that draws itself between two elements on scroll. */
5
5
  declare class ScrollArrow {
@@ -22,9 +22,25 @@ declare class ScrollArrow {
22
22
  private labelBgEl;
23
23
  private progress;
24
24
  private ro?;
25
+ /**
26
+ * Lazily armed only while an anchor is hidden/zero-size, so the common path
27
+ * (both anchors visible) wires no extra observer. Fires render() on reveal.
28
+ */
29
+ private revealObs?;
25
30
  private rafId;
26
31
  private destroyed;
32
+ /** Render static + fully drawn: user prefers reduced motion and we honor it. */
33
+ private reducedMotion;
34
+ /** When false the arrow is hidden and skips all draw/scroll work (no teardown). */
35
+ private enabled;
27
36
  constructor(options: ScrollArrowOptions);
37
+ /**
38
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
39
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
40
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
41
+ * to `matchMedia` to switch arrows off below a breakpoint.
42
+ */
43
+ setEnabled(on: boolean): void;
28
44
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
29
45
  setProgress(p: number): void;
30
46
  /** Recompute geometry (call after layout changes you control). */
@@ -44,14 +60,64 @@ declare class ScrollArrow {
44
60
  * once the line is complete.
45
61
  */
46
62
  private applyProgress;
63
+ /**
64
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
65
+ * IntersectionObserver fires when a previously `display:none` element gains a
66
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
67
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
68
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
69
+ */
70
+ private armReveal;
71
+ private teardownReveal;
72
+ private bind;
73
+ private onScroll;
74
+ private update;
75
+ }
76
+
77
+ /**
78
+ * A coordinated set of arrows that draw in a staggered sequence off one shared
79
+ * scroll trigger (or manual `setProgress`). Owns its arrows; `destroy()` tears
80
+ * them all down.
81
+ */
82
+ declare class ScrollArrowGroup {
83
+ private opts;
84
+ private arrows;
85
+ private windows;
86
+ private elements;
87
+ private target;
88
+ private progress;
89
+ private rafId;
90
+ private destroyed;
91
+ /** Render every arrow static + fully drawn when reduced motion is honored. */
92
+ private reducedMotion;
93
+ /** When false every arrow is hidden and the group does no scroll work. */
94
+ private enabled;
95
+ constructor(options: ScrollArrowGroupOptions);
96
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
97
+ setProgress(p: number): void;
98
+ /** Recompute geometry for every arrow (call after layout changes). */
99
+ refresh(): void;
100
+ /**
101
+ * Suspend or restore the whole group without tearing it down. Hides every
102
+ * arrow and stops scroll work when off; restores and recomputes when on.
103
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
104
+ */
105
+ setEnabled(on: boolean): void;
106
+ destroy(): void;
47
107
  private bind;
48
108
  private onScroll;
49
109
  private update;
110
+ /** Push each arrow to its sliced local progress for the current group value. */
111
+ private distribute;
112
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
113
+ private groupRect;
50
114
  }
51
115
 
52
116
  declare function easeInOutCubic(t: number): number;
53
117
 
54
118
  /** Convenience factory. `const a = scrollArrow({ start, end })`. */
55
119
  declare function scrollArrow(options: ScrollArrowOptions): ScrollArrow;
120
+ /** Convenience factory. `const g = scrollArrowGroup({ arrows: [...] })`. */
121
+ declare function scrollArrowGroup(options: ScrollArrowGroupOptions): ScrollArrowGroup;
56
122
 
57
- export { ScrollArrow, ScrollArrowOptions, easeInOutCubic, scrollArrow };
123
+ 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-LIT577GH.js';
2
+ export { ScrollArrow, ScrollArrowGroup, easeInOutCubic } from './chunk-LIT577GH.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