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/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);
@@ -337,14 +378,36 @@ var ScrollArrow = class {
337
378
  this.svg = getOverlay(this.container);
338
379
  this.rc = rough__default.default.svg(this.svg);
339
380
  this.svg.appendChild(this.group);
340
- this.progress = clamp01(this.opts.progress);
381
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
382
+ this.progress = this.reducedMotion ? 1 : clamp01(this.opts.progress);
341
383
  this.refs = this.resolveRefs();
342
384
  this.stroke = options.stroke ?? getComputedStyle(this.container).color ?? "#222";
343
385
  this.seed = options.seed ?? deriveSeed(refKey(options.start), refKey(options.end));
386
+ this.enabled = this.opts.enabled ?? true;
387
+ if (!this.enabled) this.group.style.display = "none";
344
388
  this.render();
345
389
  this.bind();
346
390
  this.update();
347
391
  }
392
+ /**
393
+ * Suspend or restore the arrow without tearing it down. Disabling hides it and
394
+ * stops all draw/scroll work; enabling shows it and recomputes geometry (so it
395
+ * reflects any layout change that happened while hidden). Idempotent. Wire it
396
+ * to `matchMedia` to switch arrows off below a breakpoint.
397
+ */
398
+ setEnabled(on) {
399
+ if (on === this.enabled || this.destroyed) return;
400
+ this.enabled = on;
401
+ if (on) {
402
+ this.group.style.display = "";
403
+ this.render();
404
+ this.update();
405
+ } else {
406
+ this.group.style.display = "none";
407
+ cancelAnimationFrame(this.rafId);
408
+ this.rafId = 0;
409
+ }
410
+ }
348
411
  /** Manually set draw progress (0..1). Only meaningful when scroll is false. */
349
412
  setProgress(p) {
350
413
  this.progress = clamp01(p);
@@ -358,6 +421,7 @@ var ScrollArrow = class {
358
421
  destroy() {
359
422
  this.destroyed = true;
360
423
  this.ro?.disconnect();
424
+ this.teardownReveal();
361
425
  window.removeEventListener("scroll", this.onScroll, true);
362
426
  window.removeEventListener("resize", this.onScroll);
363
427
  cancelAnimationFrame(this.rafId);
@@ -383,17 +447,30 @@ var ScrollArrow = class {
383
447
  const list = Array.isArray(a) ? a : [a];
384
448
  return list.map(resolve).filter((el) => el !== null);
385
449
  }
386
- computeEndpoints() {
387
- const sr = docRect(this.refs.start);
388
- const er = docRect(this.refs.end);
450
+ computeEndpoints(sr, er) {
389
451
  const ss = this.opts.startSocket ?? "auto";
390
452
  const es = this.opts.endSocket ?? "auto";
391
- return resolveEndpoints(sr, er, ss, es);
453
+ return resolveEndpoints(
454
+ sr,
455
+ er,
456
+ ss,
457
+ es,
458
+ this.opts.startSocketOffset ?? 0,
459
+ this.opts.endSocketOffset ?? 0
460
+ );
392
461
  }
393
462
  render() {
394
463
  while (this.group.firstChild) this.group.removeChild(this.group.firstChild);
395
464
  this.segments = [];
396
- const ep = this.computeEndpoints();
465
+ if (!this.enabled) return;
466
+ const sr = docRect(this.refs.start);
467
+ const er = docRect(this.refs.end);
468
+ if (isDegenerateRect(sr) || isDegenerateRect(er)) {
469
+ this.armReveal();
470
+ return;
471
+ }
472
+ this.teardownReveal();
473
+ const ep = this.computeEndpoints(sr, er);
397
474
  const origin = overlayOrigin(this.svg);
398
475
  const shift = (e) => ({
399
476
  start: { x: e.start.x - origin.x, y: e.start.y - origin.y },
@@ -410,24 +487,29 @@ var ScrollArrow = class {
410
487
  this.seed,
411
488
  this.opts.anchorEnds ?? true
412
489
  );
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);
490
+ let d;
491
+ if (this.opts.route === "elbow") {
492
+ d = buildElbowPath(local);
493
+ } else {
494
+ const obstacles = this.resolveAvoid().map((el) => {
495
+ const dr = docRect(el);
496
+ return {
497
+ left: dr.left - origin.x,
498
+ top: dr.top - origin.y,
499
+ width: dr.width,
500
+ height: dr.height
501
+ };
502
+ });
503
+ const clear = routeOffset(
504
+ local.start,
505
+ local.end,
506
+ obstacles,
507
+ this.opts.avoidPadding ?? 14
508
+ );
509
+ const BOW = 1.6;
510
+ const belly = { x: clear.x * BOW, y: clear.y * BOW };
511
+ d = buildPath(local, curvature, belly);
512
+ }
431
513
  this.appendDrawable(this.rc.path(d, roughOpts), "line");
432
514
  const head = this.opts.head;
433
515
  const size = this.opts.headSize;
@@ -538,19 +620,37 @@ var ScrollArrow = class {
538
620
  if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
539
621
  }
540
622
  }
623
+ /**
624
+ * Watch the anchors for the moment a hidden one becomes laid out, then redraw.
625
+ * IntersectionObserver fires when a previously `display:none` element gains a
626
+ * box — a transition ResizeObserver does not reliably report. Armed lazily and
627
+ * idempotently; torn down by the next successful render(). Guarded so it no-ops
628
+ * in environments without IntersectionObserver (older engines, some SSR/jsdom).
629
+ */
630
+ armReveal() {
631
+ if (this.revealObs || this.destroyed) return;
632
+ if (typeof IntersectionObserver === "undefined") return;
633
+ this.revealObs = new IntersectionObserver(() => this.render());
634
+ this.revealObs.observe(this.refs.start);
635
+ this.revealObs.observe(this.refs.end);
636
+ }
637
+ teardownReveal() {
638
+ this.revealObs?.disconnect();
639
+ this.revealObs = void 0;
640
+ }
541
641
  bind() {
542
642
  const targets = [this.refs.start, this.refs.end, ...this.resolveAvoid()];
543
643
  if (this.refs.target) targets.push(this.refs.target);
544
644
  this.ro = new ResizeObserver(() => this.render());
545
645
  targets.forEach((t) => this.ro.observe(t));
546
- if (this.opts.scroll !== false) {
646
+ if (this.opts.scroll !== false && !this.reducedMotion) {
547
647
  window.addEventListener("scroll", this.onScroll, true);
548
648
  window.addEventListener("resize", this.onScroll);
549
649
  }
550
650
  }
551
651
  update() {
552
- if (this.destroyed) return;
553
- if (this.opts.scroll === false) {
652
+ if (this.destroyed || !this.enabled) return;
653
+ if (this.opts.scroll === false || this.reducedMotion) {
554
654
  this.applyProgress();
555
655
  return;
556
656
  }
@@ -572,6 +672,140 @@ function clampAt(t) {
572
672
  return t < 0 ? 0 : t > 1 ? 1 : t;
573
673
  }
574
674
 
675
+ // src/group.ts
676
+ var ScrollArrowGroup = class {
677
+ constructor(options) {
678
+ this.elements = [];
679
+ this.target = null;
680
+ this.progress = 0;
681
+ this.rafId = 0;
682
+ this.destroyed = false;
683
+ this.onScroll = () => {
684
+ if (this.rafId) return;
685
+ this.rafId = requestAnimationFrame(() => {
686
+ this.rafId = 0;
687
+ this.update();
688
+ });
689
+ };
690
+ if (!options.arrows || options.arrows.length === 0) {
691
+ throw new Error("[scroll-arrows] group needs at least one arrow");
692
+ }
693
+ this.opts = {
694
+ stagger: 1,
695
+ speed: 1,
696
+ easing: (t) => t,
697
+ ...options
698
+ };
699
+ this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
700
+ this.enabled = this.opts.enabled ?? true;
701
+ this.arrows = options.arrows.map(
702
+ (a) => new ScrollArrow({
703
+ ...a,
704
+ scroll: false,
705
+ progress: 0,
706
+ respectReducedMotion: false,
707
+ enabled: this.enabled
708
+ })
709
+ );
710
+ this.windows = staggerWindows(this.arrows.length, this.opts.stagger);
711
+ for (const a of options.arrows) {
712
+ const s = resolve2(a.start);
713
+ const e = resolve2(a.end);
714
+ if (s) this.elements.push(s);
715
+ if (e) this.elements.push(e);
716
+ }
717
+ const scroll = this.opts.scroll;
718
+ if (scroll !== false && scroll?.target) {
719
+ this.target = resolve2(scroll.target);
720
+ }
721
+ if (this.reducedMotion) this.progress = 1;
722
+ this.bind();
723
+ this.update();
724
+ }
725
+ /** Manually set group progress (0..1). Only meaningful when scroll is false. */
726
+ setProgress(p) {
727
+ this.progress = clamp01(p);
728
+ this.distribute();
729
+ }
730
+ /** Recompute geometry for every arrow (call after layout changes). */
731
+ refresh() {
732
+ this.arrows.forEach((a) => a.refresh());
733
+ this.update();
734
+ }
735
+ /**
736
+ * Suspend or restore the whole group without tearing it down. Hides every
737
+ * arrow and stops scroll work when off; restores and recomputes when on.
738
+ * Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
739
+ */
740
+ setEnabled(on) {
741
+ if (on === this.enabled || this.destroyed) return;
742
+ this.enabled = on;
743
+ this.arrows.forEach((a) => a.setEnabled(on));
744
+ if (on) {
745
+ this.bind();
746
+ this.update();
747
+ } else {
748
+ window.removeEventListener("scroll", this.onScroll, true);
749
+ window.removeEventListener("resize", this.onScroll);
750
+ cancelAnimationFrame(this.rafId);
751
+ this.rafId = 0;
752
+ }
753
+ }
754
+ destroy() {
755
+ this.destroyed = true;
756
+ window.removeEventListener("scroll", this.onScroll, true);
757
+ window.removeEventListener("resize", this.onScroll);
758
+ cancelAnimationFrame(this.rafId);
759
+ this.arrows.forEach((a) => a.destroy());
760
+ }
761
+ // --- internals ---------------------------------------------------------
762
+ bind() {
763
+ if (this.opts.scroll === false || this.reducedMotion || !this.enabled)
764
+ return;
765
+ window.addEventListener("scroll", this.onScroll, true);
766
+ window.addEventListener("resize", this.onScroll);
767
+ }
768
+ update() {
769
+ if (this.destroyed || !this.enabled) return;
770
+ if (this.opts.scroll === false || this.reducedMotion) {
771
+ this.distribute();
772
+ return;
773
+ }
774
+ const scroll = this.opts.scroll ?? {};
775
+ const range = scroll.range ?? [0.85, 0.35];
776
+ const rect = this.target ? docRect(this.target) : this.groupRect();
777
+ const raw = scrollProgress(rect, range);
778
+ this.progress = clamp01(raw * this.opts.speed);
779
+ this.distribute();
780
+ }
781
+ /** Push each arrow to its sliced local progress for the current group value. */
782
+ distribute() {
783
+ const eased = clamp01(this.opts.easing(this.progress));
784
+ this.arrows.forEach((arrow, i) => {
785
+ arrow.setProgress(windowProgress(eased, this.windows[i]));
786
+ });
787
+ }
788
+ /** Synthetic rect spanning every endpoint, used as the default trigger. */
789
+ groupRect() {
790
+ const rects = this.elements.map(docRect);
791
+ if (rects.length === 0) return { left: 0, top: 0, width: 0, height: 0 };
792
+ let left = Infinity;
793
+ let top = Infinity;
794
+ let right = -Infinity;
795
+ let bottom = -Infinity;
796
+ for (const r2 of rects) {
797
+ left = Math.min(left, r2.left);
798
+ top = Math.min(top, r2.top);
799
+ right = Math.max(right, r2.left + r2.width);
800
+ bottom = Math.max(bottom, r2.top + r2.height);
801
+ }
802
+ return { left, top, width: right - left, height: bottom - top };
803
+ }
804
+ };
805
+ function resolve2(ref) {
806
+ return typeof ref === "string" ? document.querySelector(ref) : ref;
807
+ }
808
+
575
809
  // src/react.tsx
576
810
  function read(a) {
577
811
  if (typeof a === "string") return a;
@@ -597,8 +831,33 @@ function ScrollArrowLine(props) {
597
831
  useScrollArrow(props);
598
832
  return null;
599
833
  }
834
+ function useScrollArrowGroup(options) {
835
+ const groupRef = react.useRef(null);
836
+ const { arrows, deps = [], ...rest } = options;
837
+ react.useEffect(() => {
838
+ const resolved = arrows.map((a) => {
839
+ const s = read(a.start);
840
+ const e = read(a.end);
841
+ if (!s || !e) return null;
842
+ return { ...a, start: s, end: e };
843
+ }).filter((a) => a !== null);
844
+ if (resolved.length === 0) return;
845
+ const group = new ScrollArrowGroup({ ...rest, arrows: resolved });
846
+ groupRef.current = group;
847
+ return () => {
848
+ group.destroy();
849
+ groupRef.current = null;
850
+ };
851
+ }, deps);
852
+ }
853
+ function ScrollArrowGroupLines(props) {
854
+ useScrollArrowGroup(props);
855
+ return null;
856
+ }
600
857
 
858
+ exports.ScrollArrowGroupLines = ScrollArrowGroupLines;
601
859
  exports.ScrollArrowLine = ScrollArrowLine;
602
860
  exports.useScrollArrow = useScrollArrow;
861
+ exports.useScrollArrowGroup = useScrollArrowGroup;
603
862
  //# sourceMappingURL=react.cjs.map
604
863
  //# sourceMappingURL=react.cjs.map
package/dist/react.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RefObject } from 'react';
2
- import { S as ScrollArrowOptions } from './types-CDd8JqZX.cjs';
2
+ import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-Cpvz9wtr.cjs';
3
3
 
4
4
  type Anchor = RefObject<Element | null> | Element | string;
5
5
  interface UseScrollArrowOptions extends Omit<ScrollArrowOptions, 'start' | 'end'> {
@@ -16,5 +16,23 @@ declare function useScrollArrow(options: UseScrollArrowOptions): void;
16
16
  type ScrollArrowProps = UseScrollArrowOptions;
17
17
  /** Declarative component form. Renders nothing itself. */
18
18
  declare function ScrollArrowLine(props: ScrollArrowProps): null;
19
+ /** One arrow in a group, with React-ref anchors. */
20
+ interface GroupArrow extends Omit<ScrollArrowOptions, 'start' | 'end'> {
21
+ start: Anchor;
22
+ end: Anchor;
23
+ }
24
+ interface UseScrollArrowGroupOptions extends Omit<ScrollArrowGroupOptions, 'arrows'> {
25
+ arrows: GroupArrow[];
26
+ /** Re-create the group when any value here changes. */
27
+ deps?: unknown[];
28
+ }
29
+ /**
30
+ * Imperatively manage a staggered ScrollArrowGroup tied to element refs.
31
+ * Returns nothing; the arrows live in an overlay <svg>, not the React tree.
32
+ */
33
+ declare function useScrollArrowGroup(options: UseScrollArrowGroupOptions): void;
34
+ type ScrollArrowGroupProps = UseScrollArrowGroupOptions;
35
+ /** Declarative group form. Renders nothing itself. */
36
+ declare function ScrollArrowGroupLines(props: ScrollArrowGroupProps): null;
19
37
 
20
- export { ScrollArrowLine, ScrollArrowOptions, type ScrollArrowProps, type UseScrollArrowOptions, useScrollArrow };
38
+ export { type GroupArrow, ScrollArrowGroupLines, ScrollArrowGroupOptions, type ScrollArrowGroupProps, ScrollArrowLine, ScrollArrowOptions, type ScrollArrowProps, type UseScrollArrowGroupOptions, type UseScrollArrowOptions, useScrollArrow, useScrollArrowGroup };
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RefObject } from 'react';
2
- import { S as ScrollArrowOptions } from './types-CDd8JqZX.js';
2
+ import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-Cpvz9wtr.js';
3
3
 
4
4
  type Anchor = RefObject<Element | null> | Element | string;
5
5
  interface UseScrollArrowOptions extends Omit<ScrollArrowOptions, 'start' | 'end'> {
@@ -16,5 +16,23 @@ declare function useScrollArrow(options: UseScrollArrowOptions): void;
16
16
  type ScrollArrowProps = UseScrollArrowOptions;
17
17
  /** Declarative component form. Renders nothing itself. */
18
18
  declare function ScrollArrowLine(props: ScrollArrowProps): null;
19
+ /** One arrow in a group, with React-ref anchors. */
20
+ interface GroupArrow extends Omit<ScrollArrowOptions, 'start' | 'end'> {
21
+ start: Anchor;
22
+ end: Anchor;
23
+ }
24
+ interface UseScrollArrowGroupOptions extends Omit<ScrollArrowGroupOptions, 'arrows'> {
25
+ arrows: GroupArrow[];
26
+ /** Re-create the group when any value here changes. */
27
+ deps?: unknown[];
28
+ }
29
+ /**
30
+ * Imperatively manage a staggered ScrollArrowGroup tied to element refs.
31
+ * Returns nothing; the arrows live in an overlay <svg>, not the React tree.
32
+ */
33
+ declare function useScrollArrowGroup(options: UseScrollArrowGroupOptions): void;
34
+ type ScrollArrowGroupProps = UseScrollArrowGroupOptions;
35
+ /** Declarative group form. Renders nothing itself. */
36
+ declare function ScrollArrowGroupLines(props: ScrollArrowGroupProps): null;
19
37
 
20
- export { ScrollArrowLine, ScrollArrowOptions, type ScrollArrowProps, type UseScrollArrowOptions, useScrollArrow };
38
+ export { type GroupArrow, ScrollArrowGroupLines, ScrollArrowGroupOptions, type ScrollArrowGroupProps, ScrollArrowLine, ScrollArrowOptions, type ScrollArrowProps, type UseScrollArrowGroupOptions, type UseScrollArrowOptions, useScrollArrow, useScrollArrowGroup };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ScrollArrow } from './chunk-GKWBGFLA.js';
1
+ import { ScrollArrow, ScrollArrowGroup } from './chunk-LIT577GH.js';
2
2
  import { useRef, useEffect } from 'react';
3
3
 
4
4
  function read(a) {
@@ -25,7 +25,30 @@ function ScrollArrowLine(props) {
25
25
  useScrollArrow(props);
26
26
  return null;
27
27
  }
28
+ function useScrollArrowGroup(options) {
29
+ const groupRef = useRef(null);
30
+ const { arrows, deps = [], ...rest } = options;
31
+ useEffect(() => {
32
+ const resolved = arrows.map((a) => {
33
+ const s = read(a.start);
34
+ const e = read(a.end);
35
+ if (!s || !e) return null;
36
+ return { ...a, start: s, end: e };
37
+ }).filter((a) => a !== null);
38
+ if (resolved.length === 0) return;
39
+ const group = new ScrollArrowGroup({ ...rest, arrows: resolved });
40
+ groupRef.current = group;
41
+ return () => {
42
+ group.destroy();
43
+ groupRef.current = null;
44
+ };
45
+ }, deps);
46
+ }
47
+ function ScrollArrowGroupLines(props) {
48
+ useScrollArrowGroup(props);
49
+ return null;
50
+ }
28
51
 
29
- export { ScrollArrowLine, useScrollArrow };
52
+ export { ScrollArrowGroupLines, ScrollArrowLine, useScrollArrow, useScrollArrowGroup };
30
53
  //# sourceMappingURL=react.js.map
31
54
  //# sourceMappingURL=react.js.map
@@ -5,6 +5,12 @@ type Point = {
5
5
  /** Which edge of an element the arrow attaches to. `auto` picks the best side. */
6
6
  type Socket = 'auto' | 'top' | 'bottom' | 'left' | 'right' | 'center';
7
7
  type ArrowHead = 'start' | 'end' | 'both' | 'none';
8
+ /**
9
+ * Line routing style. `curved` (default) is the smooth bezier with single-bend
10
+ * obstacle avoidance. `elbow` draws right-angle connectors (tree/org-chart
11
+ * brackets); `elbow` ignores `avoid`/`curvature`.
12
+ */
13
+ type Route = 'curved' | 'elbow';
8
14
  /** Anything we can resolve to a live DOM element. */
9
15
  type ElementRef = Element | string;
10
16
  interface ScrollOptions {
@@ -48,8 +54,24 @@ interface ScrollArrowOptions {
48
54
  startSocket?: Socket;
49
55
  /** Force an end edge. Default "auto". */
50
56
  endSocket?: Socket;
57
+ /**
58
+ * Slide the start attach point along its edge, as a fraction of the edge
59
+ * length: `0` (default) = centered, `-0.5`/`+0.5` = the corners. Use to fan
60
+ * out several arrows that leave the same element so they don't stack on one
61
+ * point (e.g. `-0.25`, `0`, `+0.25` for three siblings).
62
+ */
63
+ startSocketOffset?: number;
64
+ /** Slide the end attach point along its edge. See `startSocketOffset`. */
65
+ endSocketOffset?: number;
51
66
  /** Extra bow of the underlying curve, 0..1. Folded into roughness if unset. */
52
67
  curvature?: number;
68
+ /**
69
+ * Routing style. `"curved"` (default) is the smooth bezier with obstacle
70
+ * avoidance; `"elbow"` draws right-angle connectors for tree/org-chart
71
+ * diagrams. Elbow mode ignores `avoid` and `curvature`. Pair with explicit
72
+ * `startSocket`/`endSocket` for predictable bracket shapes.
73
+ */
74
+ route?: Route;
53
75
  /**
54
76
  * Pin the stroke's endpoints to the anchor sockets so the arrow always lands
55
77
  * on its targets, even at high roughness. Set false to let the scratchy ends
@@ -99,6 +121,62 @@ interface ScrollArrowOptions {
99
121
  easing?: (t: number) => number;
100
122
  /** Start fully drawn instead of empty. Useful with `scroll:false`. Default 0. */
101
123
  progress?: number;
124
+ /**
125
+ * Auto-respect `prefers-reduced-motion: reduce`. When the user prefers reduced
126
+ * motion, the arrow renders fully drawn and static (no scroll animation),
127
+ * while still tracking layout. Set false to keep the scroll animation
128
+ * regardless of the OS setting. Default true. Evaluated once at construction.
129
+ */
130
+ respectReducedMotion?: boolean;
131
+ /**
132
+ * Initial enabled state. When `false`, the arrow is created hidden and draws
133
+ * nothing until `setEnabled(true)` is called. Use with a `matchMedia` listener
134
+ * to switch arrows off below a breakpoint (where the diagram reflows) without
135
+ * destroying and rebuilding them. Default true.
136
+ */
137
+ enabled?: boolean;
138
+ }
139
+ /**
140
+ * A coordinated set of arrows that draw in a staggered sequence, driven by one
141
+ * shared scroll trigger (or manually). Each arrow gets a slice of the group's
142
+ * progress so they reveal as `A then B then C` rather than independently.
143
+ */
144
+ interface ScrollArrowGroupOptions {
145
+ /**
146
+ * The arrows in the group, in reveal order. Each entry takes the usual
147
+ * per-arrow options (roughness, stroke, label, ...); the group forces
148
+ * `scroll: false` on each and drives their progress itself.
149
+ */
150
+ arrows: ScrollArrowOptions[];
151
+ /**
152
+ * How sequential the reveal is, 0..1. `1` (default) draws each arrow in its
153
+ * own non-overlapping slice (`A` finishes before `B` starts). `0` draws them
154
+ * all at once. Values between overlap the slices.
155
+ */
156
+ stagger?: number;
157
+ /**
158
+ * Shared scroll behavior for the whole group. Pass `false` to drive the group
159
+ * manually via `setProgress()`. The `target` defaults to a synthetic rect
160
+ * spanning every arrow's endpoints, so the group reveals as it scrolls in.
161
+ */
162
+ scroll?: ScrollOptions | false;
163
+ /** Multiplier on the group's draw rate. See `ScrollArrowOptions.speed`. Default 1. */
164
+ speed?: number;
165
+ /** Easing applied to the group's overall progress before slicing. Default linear. */
166
+ easing?: (t: number) => number;
167
+ /**
168
+ * Auto-respect `prefers-reduced-motion: reduce` for the whole group. When the
169
+ * user prefers reduced motion, every arrow renders fully drawn and static (no
170
+ * scroll animation). Set false to keep the staggered scroll reveal regardless
171
+ * of the OS setting. Default true. Evaluated once at construction.
172
+ */
173
+ respectReducedMotion?: boolean;
174
+ /**
175
+ * Initial enabled state for the whole group. When `false`, every arrow starts
176
+ * hidden until `setEnabled(true)`. Pairs with a `matchMedia` listener for
177
+ * breakpoint-aware diagrams. Default true.
178
+ */
179
+ enabled?: boolean;
102
180
  }
103
181
 
104
- export type { ArrowHead as A, ElementRef as E, Point as P, ScrollArrowOptions as S, ScrollOptions as a, Socket as b };
182
+ export type { ArrowHead as A, ElementRef as E, Point as P, ScrollArrowOptions as S, ScrollArrowGroupOptions as a, ScrollOptions as b, Socket as c };