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.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);
@@ -268,6 +309,25 @@ function clamp012(t) {
268
309
  }
269
310
 
270
311
  // src/draw.ts
312
+ var LABEL_KEYWORDS = {
313
+ start: 0,
314
+ middle: 0.5,
315
+ end: 1
316
+ };
317
+ function resolveLabelAt(value, fallback = 0.5) {
318
+ if (value == null) return fallback;
319
+ if (typeof value === "number") {
320
+ return Number.isFinite(value) ? clamp01(value) : fallback;
321
+ }
322
+ const key = value.trim().toLowerCase();
323
+ const keyword = LABEL_KEYWORDS[key];
324
+ if (keyword !== void 0) return keyword;
325
+ if (key.endsWith("%")) {
326
+ const n = Number.parseFloat(key.slice(0, -1));
327
+ return Number.isFinite(n) ? clamp01(n / 100) : fallback;
328
+ }
329
+ return fallback;
330
+ }
271
331
  function lengths(segs) {
272
332
  let lineLen = 0;
273
333
  let headLen = 0;
@@ -297,7 +357,8 @@ function lineProgress(segs, eased) {
297
357
  return lineLen > 0 ? clamp01(drawn / lineLen) : 1;
298
358
  }
299
359
  function labelOpacity(lineProg, labelAt, fade = 0.08) {
300
- return clamp01((lineProg - clamp01(labelAt)) / (fade || 1));
360
+ const start = Math.min(clamp01(labelAt), 1 - fade);
361
+ return clamp01((lineProg - start) / (fade || 1));
301
362
  }
302
363
 
303
364
  // src/scroll-arrow.ts
@@ -311,8 +372,17 @@ var ScrollArrow = class {
311
372
  this.segments = [];
312
373
  /** Representative line stroke + label nodes, when a label is set. */
313
374
  this.lineEl = null;
375
+ /**
376
+ * The smooth ideal path `d` (pre-roughjs). Label placement measures against
377
+ * this, not `lineEl`: rough.js bakes its double stroke into one path with two
378
+ * subpaths, so `lineEl.getTotalLength()` is ~2x the visible curve and would
379
+ * put `labelAt` at twice its intended fraction.
380
+ */
381
+ this.lineD = "";
314
382
  this.labelEl = null;
315
383
  this.labelBgEl = null;
384
+ /** `opts.labelAt` resolved to a 0..1 fraction; cached by renderLabel for the draw loop. */
385
+ this.resolvedLabelAt = 0.5;
316
386
  this.rafId = 0;
317
387
  this.destroyed = false;
318
388
  this.onScroll = () => {
@@ -336,14 +406,36 @@ var ScrollArrow = class {
336
406
  this.svg = getOverlay(this.container);
337
407
  this.rc = rough__default.default.svg(this.svg);
338
408
  this.svg.appendChild(this.group);
339
- this.progress = clamp01(this.opts.progress);
409
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
410
+ this.progress = this.reducedMotion ? 1 : clamp01(this.opts.progress);
340
411
  this.refs = this.resolveRefs();
341
412
  this.stroke = options.stroke ?? getComputedStyle(this.container).color ?? "#222";
342
413
  this.seed = options.seed ?? deriveSeed(refKey(options.start), refKey(options.end));
414
+ this.enabled = this.opts.enabled ?? true;
415
+ if (!this.enabled) this.group.style.display = "none";
343
416
  this.render();
344
417
  this.bind();
345
418
  this.update();
346
419
  }
420
+ /**
421
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
422
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
423
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
424
+ * to `matchMedia` to switch arrows off below a breakpoint.
425
+ */
426
+ setEnabled(on) {
427
+ if (on === this.enabled || this.destroyed) return;
428
+ this.enabled = on;
429
+ if (on) {
430
+ this.group.style.display = "";
431
+ this.render();
432
+ this.update();
433
+ } else {
434
+ this.group.style.display = "none";
435
+ cancelAnimationFrame(this.rafId);
436
+ this.rafId = 0;
437
+ }
438
+ }
347
439
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
348
440
  setProgress(p) {
349
441
  this.progress = clamp01(p);
@@ -357,6 +449,7 @@ var ScrollArrow = class {
357
449
  destroy() {
358
450
  this.destroyed = true;
359
451
  this.ro?.disconnect();
452
+ this.teardownReveal();
360
453
  window.removeEventListener("scroll", this.onScroll, true);
361
454
  window.removeEventListener("resize", this.onScroll);
362
455
  cancelAnimationFrame(this.rafId);
@@ -382,17 +475,30 @@ var ScrollArrow = class {
382
475
  const list = Array.isArray(a) ? a : [a];
383
476
  return list.map(resolve).filter((el) => el !== null);
384
477
  }
385
- computeEndpoints() {
386
- const sr = docRect(this.refs.start);
387
- const er = docRect(this.refs.end);
478
+ computeEndpoints(sr, er) {
388
479
  const ss = this.opts.startSocket ?? "auto";
389
480
  const es = this.opts.endSocket ?? "auto";
390
- return resolveEndpoints(sr, er, ss, es);
481
+ return resolveEndpoints(
482
+ sr,
483
+ er,
484
+ ss,
485
+ es,
486
+ this.opts.startSocketOffset ?? 0,
487
+ this.opts.endSocketOffset ?? 0
488
+ );
391
489
  }
392
490
  render() {
393
491
  while (this.group.firstChild) this.group.removeChild(this.group.firstChild);
394
492
  this.segments = [];
395
- const ep = this.computeEndpoints();
493
+ if (!this.enabled) return;
494
+ const sr = docRect(this.refs.start);
495
+ const er = docRect(this.refs.end);
496
+ if (isDegenerateRect(sr) || isDegenerateRect(er)) {
497
+ this.armReveal();
498
+ return;
499
+ }
500
+ this.teardownReveal();
501
+ const ep = this.computeEndpoints(sr, er);
396
502
  const origin = overlayOrigin(this.svg);
397
503
  const shift = (e) => ({
398
504
  start: { x: e.start.x - origin.x, y: e.start.y - origin.y },
@@ -409,24 +515,30 @@ var ScrollArrow = class {
409
515
  this.seed,
410
516
  this.opts.anchorEnds ?? true
411
517
  );
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);
518
+ let d;
519
+ if (this.opts.route === "elbow") {
520
+ d = buildElbowPath(local);
521
+ } else {
522
+ const obstacles = this.resolveAvoid().map((el) => {
523
+ const dr = docRect(el);
524
+ return {
525
+ left: dr.left - origin.x,
526
+ top: dr.top - origin.y,
527
+ width: dr.width,
528
+ height: dr.height
529
+ };
530
+ });
531
+ const clear = routeOffset(
532
+ local.start,
533
+ local.end,
534
+ obstacles,
535
+ this.opts.avoidPadding ?? 14
536
+ );
537
+ const BOW = 1.6;
538
+ const belly = { x: clear.x * BOW, y: clear.y * BOW };
539
+ d = buildPath(local, curvature, belly);
540
+ }
541
+ this.lineD = d;
430
542
  this.appendDrawable(this.rc.path(d, roughOpts), "line");
431
543
  const head = this.opts.head;
432
544
  const size = this.opts.headSize;
@@ -463,25 +575,26 @@ var ScrollArrow = class {
463
575
  this.labelEl = null;
464
576
  this.labelBgEl = null;
465
577
  const text = this.opts.label;
466
- if (!text || !this.lineEl) return;
467
- const total = this.lineEl.getTotalLength();
468
- const at = clampAt(this.opts.labelAt ?? 0.5);
469
- const pt = this.lineEl.getPointAtLength(at * total);
578
+ if (!text || !this.lineEl || !this.lineD) return;
579
+ const at = resolveLabelAt(this.opts.labelAt);
580
+ this.resolvedLabelAt = at;
581
+ const measure = createSvgEl("path");
582
+ measure.setAttribute("d", this.lineD);
583
+ this.group.appendChild(measure);
584
+ const total = measure.getTotalLength();
585
+ const pt = measure.getPointAtLength(at * total);
470
586
  const offset = this.opts.labelOffset ?? 0;
471
587
  let x = pt.x;
472
588
  let y = pt.y;
473
589
  if (offset && total > 0) {
474
590
  const eps = Math.min(1, total / 2);
475
- const before = this.lineEl.getPointAtLength(
476
- Math.max(0, at * total - eps)
477
- );
478
- const after = this.lineEl.getPointAtLength(
479
- Math.min(total, at * total + eps)
480
- );
591
+ const before = measure.getPointAtLength(Math.max(0, at * total - eps));
592
+ const after = measure.getPointAtLength(Math.min(total, at * total + eps));
481
593
  const n = unitNormal(before, after);
482
594
  x += n.x * offset;
483
595
  y += n.y * offset;
484
596
  }
597
+ this.group.removeChild(measure);
485
598
  const label = createSvgEl("text");
486
599
  label.textContent = text;
487
600
  label.setAttribute("x", String(x));
@@ -531,25 +644,43 @@ var ScrollArrow = class {
531
644
  if (this.labelEl) {
532
645
  const op = labelOpacity(
533
646
  lineProgress(this.segments, eased),
534
- this.opts.labelAt ?? 0.5
647
+ this.resolvedLabelAt
535
648
  );
536
649
  this.labelEl.style.opacity = String(op);
537
650
  if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
538
651
  }
539
652
  }
653
+ /**
654
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
655
+ * IntersectionObserver fires when a previously `display:none` element gains a
656
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
657
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
658
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
659
+ */
660
+ armReveal() {
661
+ if (this.revealObs || this.destroyed) return;
662
+ if (typeof IntersectionObserver === "undefined") return;
663
+ this.revealObs = new IntersectionObserver(() => this.render());
664
+ this.revealObs.observe(this.refs.start);
665
+ this.revealObs.observe(this.refs.end);
666
+ }
667
+ teardownReveal() {
668
+ this.revealObs?.disconnect();
669
+ this.revealObs = void 0;
670
+ }
540
671
  bind() {
541
672
  const targets = [this.refs.start, this.refs.end, ...this.resolveAvoid()];
542
673
  if (this.refs.target) targets.push(this.refs.target);
543
674
  this.ro = new ResizeObserver(() => this.render());
544
675
  targets.forEach((t) => this.ro.observe(t));
545
- if (this.opts.scroll !== false) {
676
+ if (this.opts.scroll !== false && !this.reducedMotion) {
546
677
  window.addEventListener("scroll", this.onScroll, true);
547
678
  window.addEventListener("resize", this.onScroll);
548
679
  }
549
680
  }
550
681
  update() {
551
- if (this.destroyed) return;
552
- if (this.opts.scroll === false) {
682
+ if (this.destroyed || !this.enabled) return;
683
+ if (this.opts.scroll === false || this.reducedMotion) {
553
684
  this.applyProgress();
554
685
  return;
555
686
  }
@@ -567,17 +698,153 @@ function resolve(ref) {
567
698
  function refKey(ref) {
568
699
  return typeof ref === "string" ? ref : ref.tagName + (ref.id ? "#" + ref.id : "");
569
700
  }
570
- function clampAt(t) {
571
- return t < 0 ? 0 : t > 1 ? 1 : t;
701
+
702
+ // src/group.ts
703
+ var ScrollArrowGroup = class {
704
+ constructor(options) {
705
+ this.elements = [];
706
+ this.target = null;
707
+ this.progress = 0;
708
+ this.rafId = 0;
709
+ this.destroyed = false;
710
+ this.onScroll = () => {
711
+ if (this.rafId) return;
712
+ this.rafId = requestAnimationFrame(() => {
713
+ this.rafId = 0;
714
+ this.update();
715
+ });
716
+ };
717
+ if (!options.arrows || options.arrows.length === 0) {
718
+ throw new Error("[scroll-arrows] group needs at least one arrow");
719
+ }
720
+ this.opts = {
721
+ stagger: 1,
722
+ speed: 1,
723
+ easing: (t) => t,
724
+ ...options
725
+ };
726
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
727
+ this.enabled = this.opts.enabled ?? true;
728
+ this.arrows = options.arrows.map(
729
+ (a) => new ScrollArrow({
730
+ ...a,
731
+ scroll: false,
732
+ progress: 0,
733
+ respectReducedMotion: false,
734
+ enabled: this.enabled
735
+ })
736
+ );
737
+ this.windows = staggerWindows(this.arrows.length, this.opts.stagger);
738
+ for (const a of options.arrows) {
739
+ const s = resolve2(a.start);
740
+ const e = resolve2(a.end);
741
+ if (s) this.elements.push(s);
742
+ if (e) this.elements.push(e);
743
+ }
744
+ const scroll = this.opts.scroll;
745
+ if (scroll !== false && scroll?.target) {
746
+ this.target = resolve2(scroll.target);
747
+ }
748
+ if (this.reducedMotion) this.progress = 1;
749
+ this.bind();
750
+ this.update();
751
+ }
752
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
753
+ setProgress(p) {
754
+ this.progress = clamp01(p);
755
+ this.distribute();
756
+ }
757
+ /** Recompute geometry for every arrow (call after layout changes). */
758
+ refresh() {
759
+ this.arrows.forEach((a) => a.refresh());
760
+ this.update();
761
+ }
762
+ /**
763
+ * Suspend or restore the whole group without tearing it down. Hides every
764
+ * arrow and stops scroll work when off; restores and recomputes when on.
765
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
766
+ */
767
+ setEnabled(on) {
768
+ if (on === this.enabled || this.destroyed) return;
769
+ this.enabled = on;
770
+ this.arrows.forEach((a) => a.setEnabled(on));
771
+ if (on) {
772
+ this.bind();
773
+ this.update();
774
+ } else {
775
+ window.removeEventListener("scroll", this.onScroll, true);
776
+ window.removeEventListener("resize", this.onScroll);
777
+ cancelAnimationFrame(this.rafId);
778
+ this.rafId = 0;
779
+ }
780
+ }
781
+ destroy() {
782
+ this.destroyed = true;
783
+ window.removeEventListener("scroll", this.onScroll, true);
784
+ window.removeEventListener("resize", this.onScroll);
785
+ cancelAnimationFrame(this.rafId);
786
+ this.arrows.forEach((a) => a.destroy());
787
+ }
788
+ // --- internals ---------------------------------------------------------
789
+ bind() {
790
+ if (this.opts.scroll === false || this.reducedMotion || !this.enabled)
791
+ return;
792
+ window.addEventListener("scroll", this.onScroll, true);
793
+ window.addEventListener("resize", this.onScroll);
794
+ }
795
+ update() {
796
+ if (this.destroyed || !this.enabled) return;
797
+ if (this.opts.scroll === false || this.reducedMotion) {
798
+ this.distribute();
799
+ return;
800
+ }
801
+ const scroll = this.opts.scroll ?? {};
802
+ const range = scroll.range ?? [0.85, 0.35];
803
+ const rect = this.target ? docRect(this.target) : this.groupRect();
804
+ const raw = scrollProgress(rect, range);
805
+ this.progress = clamp01(raw * this.opts.speed);
806
+ this.distribute();
807
+ }
808
+ /** Push each arrow to its sliced local progress for the current group value. */
809
+ distribute() {
810
+ const eased = clamp01(this.opts.easing(this.progress));
811
+ this.arrows.forEach((arrow, i) => {
812
+ arrow.setProgress(windowProgress(eased, this.windows[i]));
813
+ });
814
+ }
815
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
816
+ groupRect() {
817
+ const rects = this.elements.map(docRect);
818
+ if (rects.length === 0) return { left: 0, top: 0, width: 0, height: 0 };
819
+ let left = Infinity;
820
+ let top = Infinity;
821
+ let right = -Infinity;
822
+ let bottom = -Infinity;
823
+ for (const r2 of rects) {
824
+ left = Math.min(left, r2.left);
825
+ top = Math.min(top, r2.top);
826
+ right = Math.max(right, r2.left + r2.width);
827
+ bottom = Math.max(bottom, r2.top + r2.height);
828
+ }
829
+ return { left, top, width: right - left, height: bottom - top };
830
+ }
831
+ };
832
+ function resolve2(ref) {
833
+ return typeof ref === "string" ? document.querySelector(ref) : ref;
572
834
  }
573
835
 
574
836
  // src/index.ts
575
837
  function scrollArrow(options) {
576
838
  return new ScrollArrow(options);
577
839
  }
840
+ function scrollArrowGroup(options) {
841
+ return new ScrollArrowGroup(options);
842
+ }
578
843
 
579
844
  exports.ScrollArrow = ScrollArrow;
845
+ exports.ScrollArrowGroup = ScrollArrowGroup;
580
846
  exports.easeInOutCubic = easeInOutCubic;
581
847
  exports.scrollArrow = scrollArrow;
848
+ exports.scrollArrowGroup = scrollArrowGroup;
582
849
  //# sourceMappingURL=index.cjs.map
583
850
  //# 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-CgHPWybd.cjs';
2
+ export { A as ArrowHead, E as ElementRef, L as LabelPosition, P as Point, b as ScrollOptions, c as Socket } from './types-CgHPWybd.cjs';
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 };