domet 1.0.6 → 1.1.1

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/es/index.mjs CHANGED
@@ -1,40 +1,410 @@
1
- import { useMemo, useState, useRef, useCallback, useLayoutEffect, useEffect } from 'react';
1
+ 'use client';
2
+ import { useLayoutEffect, useEffect, useMemo, useRef, useState, useCallback } from 'react';
2
3
 
3
- const DEFAULT_VISIBILITY_THRESHOLD = 0.6;
4
- const DEFAULT_HYSTERESIS_MARGIN = 150;
4
+ const DEFAULT_THRESHOLD = 0.6;
5
+ const DEFAULT_HYSTERESIS = 150;
6
+ const DEFAULT_OFFSET = 0;
7
+ const DEFAULT_THROTTLE = 10;
5
8
  const SCROLL_IDLE_MS = 100;
9
+ const MIN_SCROLL_THRESHOLD = 10;
10
+ const EDGE_TOLERANCE = 5;
11
+
12
+ const PERCENT_REGEX$1 = /^(-?\d+(?:\.\d+)?)%$/;
13
+ const VALIDATION_LIMITS = {
14
+ offset: {
15
+ min: -1e4,
16
+ max: 10000
17
+ },
18
+ offsetPercent: {
19
+ min: -500,
20
+ max: 500
21
+ },
22
+ threshold: {
23
+ min: 0,
24
+ max: 1
25
+ },
26
+ hysteresis: {
27
+ min: 0,
28
+ max: 1000
29
+ },
30
+ throttle: {
31
+ min: 0,
32
+ max: 1000
33
+ }
34
+ };
35
+ function warn(message) {
36
+ if (process.env.NODE_ENV !== "production") {
37
+ console.warn(`[domet] ${message}`);
38
+ }
39
+ }
40
+ function isFiniteNumber(value) {
41
+ return typeof value === "number" && Number.isFinite(value);
42
+ }
43
+ function clamp(value, min, max) {
44
+ return Math.max(min, Math.min(max, value));
45
+ }
46
+ function sanitizeOffset(offset) {
47
+ if (offset === undefined) {
48
+ return DEFAULT_OFFSET;
49
+ }
50
+ if (typeof offset === "number") {
51
+ if (!isFiniteNumber(offset)) {
52
+ warn(`Invalid offset value: ${offset}. Using default.`);
53
+ return DEFAULT_OFFSET;
54
+ }
55
+ const { min, max } = VALIDATION_LIMITS.offset;
56
+ if (offset < min || offset > max) {
57
+ warn(`Offset ${offset} clamped to [${min}, ${max}].`);
58
+ return clamp(offset, min, max);
59
+ }
60
+ return offset;
61
+ }
62
+ if (typeof offset === "string") {
63
+ const trimmed = offset.trim();
64
+ const match = PERCENT_REGEX$1.exec(trimmed);
65
+ if (!match) {
66
+ warn(`Invalid offset format: "${offset}". Using default.`);
67
+ return DEFAULT_OFFSET;
68
+ }
69
+ const percent = parseFloat(match[1]);
70
+ if (!isFiniteNumber(percent)) {
71
+ warn(`Invalid percentage value in offset: "${offset}". Using default.`);
72
+ return DEFAULT_OFFSET;
73
+ }
74
+ const { min, max } = VALIDATION_LIMITS.offsetPercent;
75
+ if (percent < min || percent > max) {
76
+ warn(`Offset percentage ${percent}% clamped to [${min}%, ${max}%].`);
77
+ return `${clamp(percent, min, max)}%`;
78
+ }
79
+ return trimmed;
80
+ }
81
+ warn(`Invalid offset type: ${typeof offset}. Using default.`);
82
+ return DEFAULT_OFFSET;
83
+ }
84
+ function sanitizeThreshold(threshold) {
85
+ if (threshold === undefined) {
86
+ return DEFAULT_THRESHOLD;
87
+ }
88
+ if (!isFiniteNumber(threshold)) {
89
+ warn(`Invalid threshold value: ${threshold}. Using default.`);
90
+ return DEFAULT_THRESHOLD;
91
+ }
92
+ const { min, max } = VALIDATION_LIMITS.threshold;
93
+ if (threshold < min || threshold > max) {
94
+ warn(`Threshold ${threshold} clamped to [${min}, ${max}].`);
95
+ return clamp(threshold, min, max);
96
+ }
97
+ return threshold;
98
+ }
99
+ function sanitizeHysteresis(hysteresis) {
100
+ if (hysteresis === undefined) {
101
+ return DEFAULT_HYSTERESIS;
102
+ }
103
+ if (!isFiniteNumber(hysteresis)) {
104
+ warn(`Invalid hysteresis value: ${hysteresis}. Using default.`);
105
+ return DEFAULT_HYSTERESIS;
106
+ }
107
+ const { min, max } = VALIDATION_LIMITS.hysteresis;
108
+ if (hysteresis < min || hysteresis > max) {
109
+ warn(`Hysteresis ${hysteresis} clamped to [${min}, ${max}].`);
110
+ return clamp(hysteresis, min, max);
111
+ }
112
+ return hysteresis;
113
+ }
114
+ function sanitizeThrottle(throttle) {
115
+ if (throttle === undefined) {
116
+ return DEFAULT_THROTTLE;
117
+ }
118
+ if (!isFiniteNumber(throttle)) {
119
+ warn(`Invalid throttle value: ${throttle}. Using default.`);
120
+ return DEFAULT_THROTTLE;
121
+ }
122
+ const { min, max } = VALIDATION_LIMITS.throttle;
123
+ if (throttle < min || throttle > max) {
124
+ warn(`Throttle ${throttle} clamped to [${min}, ${max}].`);
125
+ return clamp(throttle, min, max);
126
+ }
127
+ return throttle;
128
+ }
129
+ function sanitizeIds(ids) {
130
+ if (!ids || !Array.isArray(ids)) {
131
+ warn("Invalid ids: expected an array. Using empty array.");
132
+ return [];
133
+ }
134
+ const seen = new Set();
135
+ const sanitized = [];
136
+ for (const id of ids){
137
+ if (typeof id !== "string") {
138
+ warn(`Invalid id type: ${typeof id}. Skipping.`);
139
+ continue;
140
+ }
141
+ const trimmed = id.trim();
142
+ if (trimmed === "") {
143
+ warn("Empty string id detected. Skipping.");
144
+ continue;
145
+ }
146
+ if (seen.has(trimmed)) {
147
+ warn(`Duplicate id "${trimmed}" detected. Skipping.`);
148
+ continue;
149
+ }
150
+ seen.add(trimmed);
151
+ sanitized.push(trimmed);
152
+ }
153
+ return sanitized;
154
+ }
155
+ function sanitizeSelector(selector) {
156
+ if (selector === undefined) {
157
+ return "";
158
+ }
159
+ if (typeof selector !== "string") {
160
+ warn(`Invalid selector type: ${typeof selector}. Using empty string.`);
161
+ return "";
162
+ }
163
+ const trimmed = selector.trim();
164
+ if (trimmed === "") {
165
+ warn("Empty selector provided.");
166
+ }
167
+ return trimmed;
168
+ }
169
+
170
+ const PERCENT_REGEX = /^(-?\d+(?:\.\d+)?)%$/;
171
+ function resolveContainer(input) {
172
+ if (typeof window === "undefined") return null;
173
+ if (input === undefined) return null;
174
+ return input.current;
175
+ }
176
+ function resolveSectionsFromIds(ids, refs) {
177
+ return ids.map((id)=>({
178
+ id,
179
+ element: refs[id]
180
+ })).filter((s)=>s.element != null);
181
+ }
182
+ function resolveSectionsFromSelector(selector) {
183
+ if (typeof window === "undefined") return [];
184
+ try {
185
+ const elements = document.querySelectorAll(selector);
186
+ return Array.from(elements).map((el, index)=>({
187
+ id: el.id || el.dataset.domet || `section-${index}`,
188
+ element: el
189
+ }));
190
+ } catch {
191
+ if (process.env.NODE_ENV !== "production") {
192
+ console.warn(`[domet] Invalid CSS selector: "${selector}"`);
193
+ }
194
+ return [];
195
+ }
196
+ }
197
+ function resolveOffset(offset, viewportHeight, defaultOffset) {
198
+ const value = offset ?? defaultOffset;
199
+ const safeViewportHeight = Number.isFinite(viewportHeight) ? viewportHeight : 0;
200
+ if (typeof value === "number") {
201
+ return Number.isFinite(value) ? value : 0;
202
+ }
203
+ const match = PERCENT_REGEX.exec(value);
204
+ if (match) {
205
+ const percent = parseFloat(match[1]);
206
+ if (!Number.isFinite(percent)) return 0;
207
+ return percent / 100 * safeViewportHeight;
208
+ }
209
+ return 0;
210
+ }
211
+
212
+ function getSectionBounds(sections, container) {
213
+ const scrollTop = container ? container.scrollTop : window.scrollY;
214
+ const containerTop = container ? container.getBoundingClientRect().top : 0;
215
+ return sections.map(({ id, element })=>{
216
+ const rect = element.getBoundingClientRect();
217
+ const relativeTop = container ? rect.top - containerTop + scrollTop : rect.top + window.scrollY;
218
+ return {
219
+ id,
220
+ top: relativeTop,
221
+ bottom: relativeTop + rect.height,
222
+ height: rect.height
223
+ };
224
+ });
225
+ }
226
+ function calculateSectionScores(sectionBounds, sections, ctx) {
227
+ const { scrollY, viewportHeight, effectiveOffset, visibilityThreshold} = ctx;
228
+ const viewportTop = scrollY;
229
+ const viewportBottom = scrollY + viewportHeight;
230
+ const maxScroll = Math.max(1, ctx.scrollHeight - viewportHeight);
231
+ const scrollProgress = Math.min(1, Math.max(0, scrollY / maxScroll));
232
+ const dynamicOffset = effectiveOffset + scrollProgress * (viewportHeight - effectiveOffset);
233
+ const triggerLine = scrollY + dynamicOffset;
234
+ const elementMap = new Map(sections.map((s)=>[
235
+ s.id,
236
+ s.element
237
+ ]));
238
+ return sectionBounds.map((section)=>{
239
+ const visibleTop = Math.max(section.top, viewportTop);
240
+ const visibleBottom = Math.min(section.bottom, viewportBottom);
241
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
242
+ const visibilityRatio = section.height > 0 ? visibleHeight / section.height : 0;
243
+ const visibleInViewportRatio = viewportHeight > 0 ? visibleHeight / viewportHeight : 0;
244
+ const isInView = section.bottom > viewportTop && section.top < viewportBottom;
245
+ const sectionProgress = (()=>{
246
+ if (section.height === 0) return 0;
247
+ const entryPoint = viewportBottom;
248
+ const totalTravel = viewportHeight + section.height;
249
+ const traveled = entryPoint - section.top;
250
+ return Math.max(0, Math.min(1, traveled / totalTravel));
251
+ })();
252
+ let score = 0;
253
+ if (visibilityRatio >= visibilityThreshold) {
254
+ score += 1000 + visibilityRatio * 500;
255
+ } else if (isInView) {
256
+ score += visibleInViewportRatio * 800;
257
+ }
258
+ if (isInView) {
259
+ const containsTriggerLine = triggerLine >= section.top && triggerLine < section.bottom;
260
+ if (containsTriggerLine) {
261
+ score += 300;
262
+ }
263
+ const sectionCenter = section.top + section.height / 2;
264
+ const distanceFromTrigger = Math.abs(sectionCenter - triggerLine);
265
+ const maxDistance = viewportHeight;
266
+ const proximityScore = Math.max(0, 1 - distanceFromTrigger / maxDistance) * 500;
267
+ score += proximityScore;
268
+ }
269
+ const element = elementMap.get(section.id);
270
+ const rect = element ? element.getBoundingClientRect() : null;
271
+ return {
272
+ id: section.id,
273
+ score,
274
+ visibilityRatio,
275
+ inView: isInView,
276
+ bounds: section,
277
+ progress: sectionProgress,
278
+ rect
279
+ };
280
+ });
281
+ }
282
+ function determineActiveSection(scores, sectionIds, currentActiveId, hysteresisMargin, scrollY, viewportHeight, scrollHeight) {
283
+ if (scores.length === 0 || sectionIds.length === 0) return null;
284
+ const scoredIds = new Set(scores.map((s)=>s.id));
285
+ const maxScroll = Math.max(0, scrollHeight - viewportHeight);
286
+ const hasScroll = maxScroll > MIN_SCROLL_THRESHOLD;
287
+ const isAtBottom = hasScroll && scrollY + viewportHeight >= scrollHeight - EDGE_TOLERANCE;
288
+ const isAtTop = hasScroll && scrollY <= EDGE_TOLERANCE;
289
+ if (isAtBottom && sectionIds.length >= 2) {
290
+ const lastId = sectionIds[sectionIds.length - 1];
291
+ const secondLastId = sectionIds[sectionIds.length - 2];
292
+ const secondLastScore = scores.find((s)=>s.id === secondLastId);
293
+ const secondLastNotVisible = !secondLastScore || !secondLastScore.inView;
294
+ if (scoredIds.has(lastId) && secondLastNotVisible) {
295
+ return lastId;
296
+ }
297
+ }
298
+ if (isAtTop && sectionIds.length >= 2) {
299
+ const firstId = sectionIds[0];
300
+ const secondId = sectionIds[1];
301
+ const secondScore = scores.find((s)=>s.id === secondId);
302
+ const secondNotVisible = !secondScore || !secondScore.inView;
303
+ if (scoredIds.has(firstId) && secondNotVisible) {
304
+ return firstId;
305
+ }
306
+ }
307
+ const visibleScores = scores.filter((s)=>s.inView);
308
+ const candidates = visibleScores.length > 0 ? visibleScores : scores;
309
+ const sorted = [
310
+ ...candidates
311
+ ].sort((a, b)=>b.score - a.score);
312
+ if (sorted.length === 0) return null;
313
+ const bestCandidate = sorted[0];
314
+ const currentScore = scores.find((s)=>s.id === currentActiveId);
315
+ const shouldSwitch = !currentScore || !currentScore.inView || bestCandidate.score > currentScore.score + hysteresisMargin || bestCandidate.id === currentActiveId;
316
+ return shouldSwitch ? bestCandidate.id : currentActiveId;
317
+ }
318
+
6
319
  const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
7
- function useDomet(sectionIds, containerRef = null, options = {}) {
8
- const { offset = 0, offsetRatio = 0.08, debounceMs = 10, visibilityThreshold = DEFAULT_VISIBILITY_THRESHOLD, hysteresisMargin = DEFAULT_HYSTERESIS_MARGIN, behavior = "auto", onActiveChange, onSectionEnter, onSectionLeave, onScrollStart, onScrollEnd } = options;
9
- JSON.stringify(sectionIds);
10
- const stableSectionIds = useMemo(()=>sectionIds, [
11
- sectionIds
320
+ function areIdInputsEqual(a, b) {
321
+ if (Object.is(a, b)) return true;
322
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
323
+ if (a.length !== b.length) return false;
324
+ for(let i = 0; i < a.length; i++){
325
+ if (!Object.is(a[i], b[i])) return false;
326
+ }
327
+ return true;
328
+ }
329
+
330
+ function useDomet(options) {
331
+ const { container: containerInput, tracking, scrolling, onActive, onEnter, onLeave, onScrollStart, onScrollEnd } = options;
332
+ const trackingOffset = sanitizeOffset(tracking?.offset);
333
+ const throttle = sanitizeThrottle(tracking?.throttle);
334
+ const threshold = sanitizeThreshold(tracking?.threshold);
335
+ const hysteresis = sanitizeHysteresis(tracking?.hysteresis);
336
+ const scrollingDefaults = useMemo(()=>{
337
+ if (!scrolling) {
338
+ return {
339
+ behavior: "auto",
340
+ offset: undefined,
341
+ position: undefined,
342
+ lockActive: undefined
343
+ };
344
+ }
345
+ return {
346
+ behavior: scrolling.behavior ?? "auto",
347
+ offset: scrolling.offset !== undefined ? sanitizeOffset(scrolling.offset) : undefined,
348
+ position: scrolling.position,
349
+ lockActive: scrolling.lockActive
350
+ };
351
+ }, [
352
+ scrolling
12
353
  ]);
13
- const sectionIndexMap = useMemo(()=>{
14
- const map = new Map();
15
- for(let i = 0; i < stableSectionIds.length; i++){
16
- map.set(stableSectionIds[i], i);
354
+ const rawIds = "ids" in options ? options.ids : undefined;
355
+ const rawSelector = "selector" in options ? options.selector : undefined;
356
+ const idsCacheRef = useRef({
357
+ raw: undefined,
358
+ sanitized: undefined
359
+ });
360
+ const idsArray = useMemo(()=>{
361
+ if (rawIds === undefined) {
362
+ idsCacheRef.current = {
363
+ raw: undefined,
364
+ sanitized: undefined
365
+ };
366
+ return undefined;
17
367
  }
18
- return map;
368
+ if (areIdInputsEqual(rawIds, idsCacheRef.current.raw)) {
369
+ idsCacheRef.current.raw = rawIds;
370
+ return idsCacheRef.current.sanitized;
371
+ }
372
+ const sanitized = sanitizeIds(rawIds);
373
+ idsCacheRef.current = {
374
+ raw: rawIds,
375
+ sanitized
376
+ };
377
+ return sanitized;
19
378
  }, [
20
- stableSectionIds
379
+ rawIds
21
380
  ]);
22
- const [activeId, setActiveId] = useState(stableSectionIds[0] || null);
381
+ const selectorString = useMemo(()=>{
382
+ if (rawSelector === undefined) return undefined;
383
+ return sanitizeSelector(rawSelector);
384
+ }, [
385
+ rawSelector
386
+ ]);
387
+ const useSelector = selectorString !== undefined && selectorString !== "";
388
+ const initialActiveId = idsArray && idsArray.length > 0 ? idsArray[0] : null;
389
+ const [containerElement, setContainerElement] = useState(null);
390
+ const [resolvedSections, setResolvedSections] = useState([]);
391
+ const [activeId, setActiveId] = useState(initialActiveId);
23
392
  const [scroll, setScroll] = useState({
24
393
  y: 0,
25
394
  progress: 0,
26
395
  direction: null,
27
396
  velocity: 0,
28
- isScrolling: false,
397
+ scrolling: false,
29
398
  maxScroll: 0,
30
399
  viewportHeight: 0,
31
- offset: 0
400
+ trackingOffset: 0,
401
+ triggerLine: 0
32
402
  });
33
403
  const [sections, setSections] = useState({});
34
- const [containerElement, setContainerElement] = useState(null);
35
404
  const refs = useRef({});
36
405
  const refCallbacks = useRef({});
37
- const activeIdRef = useRef(stableSectionIds[0] || null);
406
+ const registerPropsCache = useRef({});
407
+ const activeIdRef = useRef(initialActiveId);
38
408
  const lastScrollY = useRef(0);
39
409
  const lastScrollTime = useRef(Date.now());
40
410
  const rafId = useRef(null);
@@ -42,50 +412,174 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
42
412
  const throttleTimeoutId = useRef(null);
43
413
  const hasPendingScroll = useRef(false);
44
414
  const isProgrammaticScrolling = useRef(false);
45
- const programmaticScrollTimeoutId = useRef(null);
46
415
  const isScrollingRef = useRef(false);
47
416
  const scrollIdleTimeoutRef = useRef(null);
48
417
  const prevSectionsInViewport = useRef(new Set());
49
418
  const recalculateRef = useRef(()=>{});
419
+ const scheduleRecalculate = useCallback(()=>{
420
+ if (typeof window === "undefined") return;
421
+ if (rafId.current) {
422
+ cancelAnimationFrame(rafId.current);
423
+ }
424
+ rafId.current = requestAnimationFrame(()=>{
425
+ rafId.current = null;
426
+ recalculateRef.current();
427
+ });
428
+ }, []);
50
429
  const scrollCleanupRef = useRef(null);
430
+ const mutationObserverRef = useRef(null);
431
+ const mutationDebounceRef = useRef(null);
432
+ const optionsRef = useRef({
433
+ trackingOffset,
434
+ scrolling: scrollingDefaults
435
+ });
51
436
  const callbackRefs = useRef({
52
- onActiveChange,
53
- onSectionEnter,
54
- onSectionLeave,
437
+ onActive,
438
+ onEnter,
439
+ onLeave,
55
440
  onScrollStart,
56
441
  onScrollEnd
57
442
  });
58
- callbackRefs.current = {
59
- onActiveChange,
60
- onSectionEnter,
61
- onSectionLeave,
443
+ useIsomorphicLayoutEffect(()=>{
444
+ optionsRef.current = {
445
+ trackingOffset,
446
+ scrolling: scrollingDefaults
447
+ };
448
+ }, [
449
+ trackingOffset,
450
+ scrollingDefaults
451
+ ]);
452
+ useEffect(()=>{
453
+ scheduleRecalculate();
454
+ }, [
455
+ trackingOffset,
456
+ scheduleRecalculate
457
+ ]);
458
+ useIsomorphicLayoutEffect(()=>{
459
+ callbackRefs.current = {
460
+ onActive,
461
+ onEnter,
462
+ onLeave,
463
+ onScrollStart,
464
+ onScrollEnd
465
+ };
466
+ }, [
467
+ onActive,
468
+ onEnter,
469
+ onLeave,
62
470
  onScrollStart,
63
471
  onScrollEnd
64
- };
65
- const getEffectiveOffset = useCallback(()=>{
66
- return offset;
472
+ ]);
473
+ const sectionIds = useMemo(()=>{
474
+ if (!useSelector && idsArray) return idsArray;
475
+ return resolvedSections.map((s)=>s.id);
67
476
  }, [
68
- offset
477
+ useSelector,
478
+ idsArray,
479
+ resolvedSections
69
480
  ]);
70
- const getScrollBehavior = useCallback(()=>{
71
- if (behavior === "auto") {
72
- if (typeof window === "undefined") return "instant";
73
- const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
74
- return prefersReducedMotion ? "instant" : "smooth";
481
+ const sectionIndexMap = useMemo(()=>{
482
+ const map = new Map();
483
+ for(let i = 0; i < sectionIds.length; i++){
484
+ map.set(sectionIds[i], i);
75
485
  }
76
- return behavior;
486
+ return map;
77
487
  }, [
78
- behavior
488
+ sectionIds
79
489
  ]);
490
+ const containerRefCurrent = containerInput?.current ?? null;
80
491
  useIsomorphicLayoutEffect(()=>{
81
- var _ref;
82
- const nextContainer = (_ref = containerRef == null ? void 0 : containerRef.current) != null ? _ref : null;
83
- if (nextContainer !== containerElement) {
84
- setContainerElement(nextContainer);
492
+ const resolved = resolveContainer(containerInput);
493
+ if (resolved !== containerElement) {
494
+ setContainerElement(resolved);
495
+ }
496
+ }, [
497
+ containerInput,
498
+ containerRefCurrent
499
+ ]);
500
+ const updateSectionsFromSelector = useCallback((selector)=>{
501
+ const resolved = resolveSectionsFromSelector(selector);
502
+ setResolvedSections(resolved);
503
+ if (resolved.length > 0) {
504
+ const currentStillExists = resolved.some((s)=>s.id === activeIdRef.current);
505
+ if (!activeIdRef.current || !currentStillExists) {
506
+ activeIdRef.current = resolved[0].id;
507
+ setActiveId(resolved[0].id);
508
+ }
509
+ } else if (activeIdRef.current !== null) {
510
+ activeIdRef.current = null;
511
+ setActiveId(null);
512
+ }
513
+ }, []);
514
+ useIsomorphicLayoutEffect(()=>{
515
+ if (useSelector && selectorString) {
516
+ updateSectionsFromSelector(selectorString);
517
+ }
518
+ }, [
519
+ selectorString,
520
+ useSelector,
521
+ updateSectionsFromSelector
522
+ ]);
523
+ useEffect(()=>{
524
+ if (!useSelector || !selectorString || typeof window === "undefined" || typeof MutationObserver === "undefined") {
525
+ return;
85
526
  }
527
+ const handleMutation = ()=>{
528
+ if (mutationDebounceRef.current) {
529
+ clearTimeout(mutationDebounceRef.current);
530
+ }
531
+ mutationDebounceRef.current = setTimeout(()=>{
532
+ updateSectionsFromSelector(selectorString);
533
+ }, 50);
534
+ };
535
+ mutationObserverRef.current = new MutationObserver(handleMutation);
536
+ mutationObserverRef.current.observe(document.body, {
537
+ childList: true,
538
+ subtree: true,
539
+ attributes: true,
540
+ attributeFilter: [
541
+ "id",
542
+ "data-domet"
543
+ ]
544
+ });
545
+ return ()=>{
546
+ if (mutationDebounceRef.current) {
547
+ clearTimeout(mutationDebounceRef.current);
548
+ mutationDebounceRef.current = null;
549
+ }
550
+ if (mutationObserverRef.current) {
551
+ mutationObserverRef.current.disconnect();
552
+ mutationObserverRef.current = null;
553
+ }
554
+ };
86
555
  }, [
87
- containerRef,
88
- containerElement
556
+ useSelector,
557
+ selectorString,
558
+ updateSectionsFromSelector
559
+ ]);
560
+ useEffect(()=>{
561
+ if (!useSelector && idsArray) {
562
+ const idsSet = new Set(idsArray);
563
+ for (const id of Object.keys(refs.current)){
564
+ if (!idsSet.has(id)) {
565
+ delete refs.current[id];
566
+ }
567
+ }
568
+ for (const id of Object.keys(refCallbacks.current)){
569
+ if (!idsSet.has(id)) {
570
+ delete refCallbacks.current[id];
571
+ }
572
+ }
573
+ const currentActive = activeIdRef.current;
574
+ const nextActive = currentActive && idsSet.has(currentActive) ? currentActive : idsArray[0] ?? null;
575
+ if (nextActive !== currentActive) {
576
+ activeIdRef.current = nextActive;
577
+ setActiveId(nextActive);
578
+ }
579
+ }
580
+ }, [
581
+ idsArray,
582
+ useSelector
89
583
  ]);
90
584
  const registerRef = useCallback((id)=>{
91
585
  const existing = refCallbacks.current[id];
@@ -96,323 +590,341 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
96
590
  } else {
97
591
  delete refs.current[id];
98
592
  }
593
+ scheduleRecalculate();
99
594
  };
100
595
  refCallbacks.current[id] = callback;
101
596
  return callback;
102
- }, []);
103
- const scrollToSection = useCallback((id)=>{
104
- if (!stableSectionIds.includes(id)) {
105
- if (process.env.NODE_ENV !== "production") {
106
- console.warn(`[domet] scrollToSection: id "${id}" not in sectionIds`);
597
+ }, [
598
+ scheduleRecalculate
599
+ ]);
600
+ const getResolvedBehavior = useCallback((behaviorOverride)=>{
601
+ const b = behaviorOverride ?? optionsRef.current.scrolling.behavior;
602
+ if (b === "auto") {
603
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
604
+ return "smooth";
107
605
  }
108
- return;
606
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
607
+ return prefersReducedMotion ? "instant" : "smooth";
109
608
  }
110
- const element = refs.current[id];
111
- if (!element) return;
112
- if (programmaticScrollTimeoutId.current) {
113
- clearTimeout(programmaticScrollTimeoutId.current);
609
+ return b;
610
+ }, []);
611
+ const getCurrentSections = useCallback(()=>{
612
+ if (!useSelector && idsArray) {
613
+ return resolveSectionsFromIds(idsArray, refs.current);
114
614
  }
115
- scrollCleanupRef.current == null ? void 0 : scrollCleanupRef.current.call(scrollCleanupRef);
116
- isProgrammaticScrolling.current = true;
117
- activeIdRef.current = id;
118
- setActiveId(id);
615
+ return resolvedSections;
616
+ }, [
617
+ useSelector,
618
+ idsArray,
619
+ resolvedSections
620
+ ]);
621
+ const scrollTo = useCallback((target, scrollOptions)=>{
622
+ const resolvedTarget = typeof target === "string" ? {
623
+ type: "id",
624
+ id: target
625
+ } : "id" in target ? {
626
+ type: "id",
627
+ id: target.id
628
+ } : {
629
+ type: "top",
630
+ top: target.top
631
+ };
632
+ const defaultScroll = optionsRef.current.scrolling;
633
+ const lockActive = scrollOptions?.lockActive ?? defaultScroll.lockActive ?? resolvedTarget.type === "id";
119
634
  const container = containerElement;
120
- const elementRect = element.getBoundingClientRect();
121
- const effectiveOffset = getEffectiveOffset() + 10;
122
635
  const scrollTarget = container || window;
123
- const unlockScroll = ()=>{
124
- isProgrammaticScrolling.current = false;
125
- if (programmaticScrollTimeoutId.current) {
126
- clearTimeout(programmaticScrollTimeoutId.current);
127
- programmaticScrollTimeoutId.current = null;
636
+ const viewportHeight = container ? container.clientHeight : window.innerHeight;
637
+ const scrollHeight = container ? container.scrollHeight : document.documentElement.scrollHeight;
638
+ const maxScroll = Math.max(0, scrollHeight - viewportHeight);
639
+ const scrollBehavior = getResolvedBehavior(scrollOptions?.behavior ?? defaultScroll.behavior);
640
+ const offsetCandidate = scrollOptions?.offset ?? defaultScroll.offset;
641
+ const offsetValue = sanitizeOffset(offsetCandidate);
642
+ const effectiveOffset = resolveOffset(offsetValue, viewportHeight, DEFAULT_OFFSET);
643
+ const stopProgrammaticScroll = ()=>{
644
+ if (scrollCleanupRef.current) {
645
+ scrollCleanupRef.current();
646
+ scrollCleanupRef.current = null;
128
647
  }
129
- requestAnimationFrame(()=>{
130
- recalculateRef.current();
131
- });
648
+ isProgrammaticScrolling.current = false;
132
649
  };
133
- let debounceTimer = null;
134
- let isUnlocked = false;
135
- const cleanup = ()=>{
136
- if (debounceTimer) {
137
- clearTimeout(debounceTimer);
138
- debounceTimer = null;
139
- }
140
- scrollTarget.removeEventListener("scroll", handleScrollActivity);
650
+ if (!lockActive) {
651
+ stopProgrammaticScroll();
652
+ } else if (scrollCleanupRef.current) {
653
+ scrollCleanupRef.current();
654
+ }
655
+ const setupLock = ()=>{
656
+ const unlockScroll = ()=>{
657
+ isProgrammaticScrolling.current = false;
658
+ };
659
+ let debounceTimer = null;
660
+ let isUnlocked = false;
661
+ const cleanup = ()=>{
662
+ if (debounceTimer) {
663
+ clearTimeout(debounceTimer);
664
+ debounceTimer = null;
665
+ }
666
+ scrollTarget.removeEventListener("scroll", handleScrollActivity);
667
+ if ("onscrollend" in scrollTarget) {
668
+ scrollTarget.removeEventListener("scrollend", handleScrollEnd);
669
+ }
670
+ scrollCleanupRef.current = null;
671
+ };
672
+ const doUnlock = ()=>{
673
+ if (isUnlocked) return;
674
+ isUnlocked = true;
675
+ cleanup();
676
+ unlockScroll();
677
+ };
678
+ const resetDebounce = ()=>{
679
+ if (debounceTimer) {
680
+ clearTimeout(debounceTimer);
681
+ }
682
+ debounceTimer = setTimeout(doUnlock, SCROLL_IDLE_MS);
683
+ };
684
+ const handleScrollActivity = ()=>{
685
+ resetDebounce();
686
+ };
687
+ const handleScrollEnd = ()=>{
688
+ doUnlock();
689
+ };
690
+ scrollTarget.addEventListener("scroll", handleScrollActivity, {
691
+ passive: true
692
+ });
141
693
  if ("onscrollend" in scrollTarget) {
142
- scrollTarget.removeEventListener("scrollend", handleScrollEnd);
694
+ scrollTarget.addEventListener("scrollend", handleScrollEnd, {
695
+ once: true
696
+ });
143
697
  }
144
- scrollCleanupRef.current = null;
145
- };
146
- const doUnlock = ()=>{
147
- if (isUnlocked) return;
148
- isUnlocked = true;
149
- cleanup();
150
- unlockScroll();
698
+ scrollCleanupRef.current = cleanup;
699
+ return {
700
+ doUnlock,
701
+ resetDebounce
702
+ };
151
703
  };
152
- const resetDebounce = ()=>{
153
- if (debounceTimer) {
154
- clearTimeout(debounceTimer);
704
+ const clampValue = (value, min, max)=>Math.max(min, Math.min(max, value));
705
+ let targetScroll = null;
706
+ let activeTargetId = null;
707
+ if (resolvedTarget.type === "id") {
708
+ const id = resolvedTarget.id;
709
+ if (!sectionIndexMap.has(id)) {
710
+ if (process.env.NODE_ENV !== "production") {
711
+ console.warn(`[domet] scrollTo: id "${id}" not found`);
712
+ }
713
+ return;
155
714
  }
156
- debounceTimer = setTimeout(doUnlock, SCROLL_IDLE_MS);
157
- };
158
- const handleScrollActivity = ()=>{
159
- resetDebounce();
160
- };
161
- const handleScrollEnd = ()=>{
162
- doUnlock();
163
- };
164
- scrollTarget.addEventListener("scroll", handleScrollActivity, {
165
- passive: true
166
- });
167
- if ("onscrollend" in scrollTarget) {
168
- scrollTarget.addEventListener("scrollend", handleScrollEnd, {
169
- once: true
170
- });
715
+ const currentSections = getCurrentSections();
716
+ const section = currentSections.find((s)=>s.id === id);
717
+ if (!section) {
718
+ if (process.env.NODE_ENV !== "production") {
719
+ console.warn(`[domet] scrollTo: element for id "${id}" not yet mounted`);
720
+ }
721
+ return;
722
+ }
723
+ const elementRect = section.element.getBoundingClientRect();
724
+ const position = scrollOptions?.position ?? defaultScroll.position;
725
+ const sectionTop = container ? elementRect.top - container.getBoundingClientRect().top + container.scrollTop : elementRect.top + window.scrollY;
726
+ const sectionHeight = elementRect.height;
727
+ const calculateTargetScroll = ()=>{
728
+ if (maxScroll <= 0) return 0;
729
+ const topTarget = sectionTop - effectiveOffset;
730
+ const centerTarget = sectionTop - (viewportHeight - sectionHeight) / 2;
731
+ const bottomTarget = sectionTop + sectionHeight - viewportHeight;
732
+ if (position === "top") {
733
+ return clampValue(topTarget, 0, maxScroll);
734
+ }
735
+ if (position === "center") {
736
+ return clampValue(centerTarget, 0, maxScroll);
737
+ }
738
+ if (position === "bottom") {
739
+ return clampValue(bottomTarget, 0, maxScroll);
740
+ }
741
+ const fits = sectionHeight <= viewportHeight;
742
+ const dynamicRange = viewportHeight - effectiveOffset;
743
+ const denominator = dynamicRange !== 0 ? 1 + dynamicRange / maxScroll : 1;
744
+ const triggerMin = (sectionTop - effectiveOffset) / denominator;
745
+ const triggerMax = (sectionTop + sectionHeight - effectiveOffset) / denominator;
746
+ if (fits) {
747
+ if (centerTarget >= triggerMin && centerTarget <= triggerMax) {
748
+ return clampValue(centerTarget, 0, maxScroll);
749
+ }
750
+ if (centerTarget < triggerMin) {
751
+ return clampValue(triggerMin, 0, maxScroll);
752
+ }
753
+ return clampValue(triggerMax, 0, maxScroll);
754
+ }
755
+ return clampValue(topTarget, 0, maxScroll);
756
+ };
757
+ targetScroll = calculateTargetScroll();
758
+ activeTargetId = id;
759
+ } else {
760
+ const top = resolvedTarget.top;
761
+ if (!Number.isFinite(top)) {
762
+ if (process.env.NODE_ENV !== "production") {
763
+ console.warn(`[domet] scrollTo: top "${top}" is not a valid number`);
764
+ }
765
+ return;
766
+ }
767
+ targetScroll = clampValue(top - effectiveOffset, 0, maxScroll);
171
768
  }
172
- scrollCleanupRef.current = cleanup;
173
- const scrollBehavior = getScrollBehavior();
769
+ if (targetScroll === null) return;
770
+ if (lockActive) {
771
+ isProgrammaticScrolling.current = true;
772
+ if (activeTargetId) {
773
+ activeIdRef.current = activeTargetId;
774
+ setActiveId(activeTargetId);
775
+ }
776
+ }
777
+ const lockControls = lockActive ? setupLock() : null;
174
778
  if (container) {
175
- const containerRect = container.getBoundingClientRect();
176
- const relativeTop = elementRect.top - containerRect.top + container.scrollTop;
177
779
  container.scrollTo({
178
- top: relativeTop - effectiveOffset,
780
+ top: targetScroll,
179
781
  behavior: scrollBehavior
180
782
  });
181
783
  } else {
182
- const absoluteTop = elementRect.top + window.scrollY;
183
784
  window.scrollTo({
184
- top: absoluteTop - effectiveOffset,
785
+ top: targetScroll,
185
786
  behavior: scrollBehavior
186
787
  });
187
788
  }
188
- if (scrollBehavior === "instant") {
189
- doUnlock();
190
- } else {
191
- resetDebounce();
789
+ if (lockControls) {
790
+ if (scrollBehavior === "instant") {
791
+ lockControls.doUnlock();
792
+ } else {
793
+ lockControls.resetDebounce();
794
+ }
192
795
  }
193
796
  }, [
194
- stableSectionIds,
797
+ sectionIndexMap,
195
798
  containerElement,
196
- getEffectiveOffset,
197
- getScrollBehavior
799
+ getResolvedBehavior,
800
+ getCurrentSections
198
801
  ]);
199
- const sectionProps = useCallback((id)=>({
802
+ const register = useCallback((id)=>{
803
+ const cached = registerPropsCache.current[id];
804
+ if (cached) return cached;
805
+ const props = {
200
806
  id,
201
807
  ref: registerRef(id),
202
808
  "data-domet": id
203
- }), [
809
+ };
810
+ registerPropsCache.current[id] = props;
811
+ return props;
812
+ }, [
204
813
  registerRef
205
814
  ]);
206
- const navProps = useCallback((id)=>({
207
- onClick: ()=>scrollToSection(id),
815
+ const link = useCallback((id, options)=>({
816
+ onClick: ()=>scrollTo(id, options),
208
817
  "aria-current": activeId === id ? "page" : undefined,
209
818
  "data-active": activeId === id
210
819
  }), [
211
820
  activeId,
212
- scrollToSection
213
- ]);
214
- useEffect(()=>{
215
- var _stableSectionIds_;
216
- const idsSet = new Set(stableSectionIds);
217
- for (const id of Object.keys(refs.current)){
218
- if (!idsSet.has(id)) {
219
- delete refs.current[id];
220
- }
221
- }
222
- for (const id of Object.keys(refCallbacks.current)){
223
- if (!idsSet.has(id)) {
224
- delete refCallbacks.current[id];
225
- }
226
- }
227
- const currentActive = activeIdRef.current;
228
- const nextActive = currentActive && idsSet.has(currentActive) ? currentActive : (_stableSectionIds_ = stableSectionIds[0]) != null ? _stableSectionIds_ : null;
229
- if (nextActive !== currentActive) {
230
- activeIdRef.current = nextActive;
231
- }
232
- setActiveId((prev)=>prev !== nextActive ? nextActive : prev);
233
- }, [
234
- stableSectionIds
235
- ]);
236
- const getSectionBounds = useCallback(()=>{
237
- const container = containerElement;
238
- const scrollTop = container ? container.scrollTop : window.scrollY;
239
- const containerTop = container ? container.getBoundingClientRect().top : 0;
240
- return stableSectionIds.map((id)=>{
241
- const el = refs.current[id];
242
- if (!el) return null;
243
- const rect = el.getBoundingClientRect();
244
- const relativeTop = container ? rect.top - containerTop + scrollTop : rect.top + window.scrollY;
245
- return {
246
- id,
247
- top: relativeTop,
248
- bottom: relativeTop + rect.height,
249
- height: rect.height
250
- };
251
- }).filter((bounds)=>bounds !== null);
252
- }, [
253
- stableSectionIds,
254
- containerElement
821
+ scrollTo
255
822
  ]);
256
823
  const calculateActiveSection = useCallback(()=>{
257
- if (isProgrammaticScrolling.current) return;
258
824
  const container = containerElement;
259
825
  const currentActiveId = activeIdRef.current;
260
826
  const now = Date.now();
261
827
  const scrollY = container ? container.scrollTop : window.scrollY;
262
828
  const viewportHeight = container ? container.clientHeight : window.innerHeight;
263
829
  const scrollHeight = container ? container.scrollHeight : document.documentElement.scrollHeight;
264
- const maxScroll = Math.max(0, scrollHeight - viewportHeight);
265
- const scrollProgress = maxScroll > 0 ? scrollY / maxScroll : 0;
830
+ const maxScroll = Math.max(1, scrollHeight - viewportHeight);
831
+ const scrollProgress = Math.min(1, Math.max(0, scrollY / maxScroll));
266
832
  const scrollDirection = scrollY === lastScrollY.current ? null : scrollY > lastScrollY.current ? "down" : "up";
267
833
  const deltaTime = now - lastScrollTime.current;
268
834
  const deltaY = scrollY - lastScrollY.current;
269
835
  const velocity = deltaTime > 0 ? Math.abs(deltaY) / deltaTime : 0;
270
836
  lastScrollY.current = scrollY;
271
837
  lastScrollTime.current = now;
272
- const sectionBounds = getSectionBounds();
838
+ const currentSections = getCurrentSections();
839
+ const sectionBounds = getSectionBounds(currentSections, container);
273
840
  if (sectionBounds.length === 0) return;
274
- const baseOffset = getEffectiveOffset();
275
- const effectiveOffset = Math.max(baseOffset, viewportHeight * offsetRatio);
276
- const triggerLine = scrollY + effectiveOffset;
277
- const viewportTop = scrollY;
278
- const viewportBottom = scrollY + viewportHeight;
279
- const scores = sectionBounds.map((section)=>{
280
- var _sectionIndexMap_get;
281
- const visibleTop = Math.max(section.top, viewportTop);
282
- const visibleBottom = Math.min(section.bottom, viewportBottom);
283
- const visibleHeight = Math.max(0, visibleBottom - visibleTop);
284
- const visibilityRatio = section.height > 0 ? visibleHeight / section.height : 0;
285
- const visibleInViewportRatio = viewportHeight > 0 ? visibleHeight / viewportHeight : 0;
286
- const isInViewport = section.bottom > viewportTop && section.top < viewportBottom;
287
- const sectionProgress = (()=>{
288
- if (section.height === 0) return 0;
289
- const entryPoint = viewportBottom;
290
- const totalTravel = viewportHeight + section.height;
291
- const traveled = entryPoint - section.top;
292
- return Math.max(0, Math.min(1, traveled / totalTravel));
293
- })();
294
- let score = 0;
295
- if (visibilityRatio >= visibilityThreshold) {
296
- score += 1000 + visibilityRatio * 500;
297
- } else if (isInViewport) {
298
- score += visibleInViewportRatio * 800;
299
- }
300
- const sectionIndex = (_sectionIndexMap_get = sectionIndexMap.get(section.id)) != null ? _sectionIndexMap_get : 0;
301
- if (scrollDirection && isInViewport && section.top <= triggerLine && section.bottom > triggerLine) {
302
- score += 200;
303
- }
304
- score -= sectionIndex * 0.1;
305
- return {
306
- id: section.id,
307
- score,
308
- visibilityRatio,
309
- isInViewport,
310
- bounds: section,
311
- progress: sectionProgress
312
- };
313
- });
314
- const hasScroll = maxScroll > 10;
315
- const isAtBottom = hasScroll && scrollY + viewportHeight >= scrollHeight - 5;
316
- const isAtTop = hasScroll && scrollY <= 5;
317
- let newActiveId = null;
318
- if (isAtBottom && stableSectionIds.length > 0) {
319
- newActiveId = stableSectionIds[stableSectionIds.length - 1];
320
- } else if (isAtTop && stableSectionIds.length > 0) {
321
- newActiveId = stableSectionIds[0];
322
- } else {
323
- const visibleScores = scores.filter((s)=>s.isInViewport);
324
- const candidates = visibleScores.length > 0 ? visibleScores : scores;
325
- candidates.sort((a, b)=>b.score - a.score);
326
- if (candidates.length > 0) {
327
- const bestCandidate = candidates[0];
328
- const currentScore = scores.find((s)=>s.id === currentActiveId);
329
- const shouldSwitch = !currentScore || !currentScore.isInViewport || bestCandidate.score > currentScore.score + hysteresisMargin || bestCandidate.id === currentActiveId;
330
- newActiveId = shouldSwitch ? bestCandidate.id : currentActiveId;
331
- }
332
- }
333
- if (newActiveId !== currentActiveId) {
841
+ const effectiveOffset = resolveOffset(trackingOffset, viewportHeight, DEFAULT_OFFSET);
842
+ const scores = calculateSectionScores(sectionBounds, currentSections, {
843
+ scrollY,
844
+ viewportHeight,
845
+ scrollHeight,
846
+ effectiveOffset,
847
+ visibilityThreshold: threshold});
848
+ const isProgrammatic = isProgrammaticScrolling.current;
849
+ const newActiveId = isProgrammatic ? currentActiveId : determineActiveSection(scores, sectionIds, currentActiveId, hysteresis, scrollY, viewportHeight, scrollHeight);
850
+ if (!isProgrammatic && newActiveId !== currentActiveId) {
334
851
  activeIdRef.current = newActiveId;
335
852
  setActiveId(newActiveId);
336
- callbackRefs.current.onActiveChange == null ? void 0 : callbackRefs.current.onActiveChange.call(callbackRefs.current, newActiveId, currentActiveId);
853
+ callbackRefs.current.onActive?.(newActiveId, currentActiveId);
337
854
  }
338
- const currentInViewport = new Set(scores.filter((s)=>s.isInViewport).map((s)=>s.id));
339
- const prevInViewport = prevSectionsInViewport.current;
340
- for (const id of currentInViewport){
341
- if (!prevInViewport.has(id)) {
342
- callbackRefs.current.onSectionEnter == null ? void 0 : callbackRefs.current.onSectionEnter.call(callbackRefs.current, id);
855
+ if (!isProgrammatic) {
856
+ const currentInViewport = new Set(scores.filter((s)=>s.inView).map((s)=>s.id));
857
+ const prevInViewport = prevSectionsInViewport.current;
858
+ for (const id of currentInViewport){
859
+ if (!prevInViewport.has(id)) {
860
+ callbackRefs.current.onEnter?.(id);
861
+ }
343
862
  }
344
- }
345
- for (const id of prevInViewport){
346
- if (!currentInViewport.has(id)) {
347
- callbackRefs.current.onSectionLeave == null ? void 0 : callbackRefs.current.onSectionLeave.call(callbackRefs.current, id);
863
+ for (const id of prevInViewport){
864
+ if (!currentInViewport.has(id)) {
865
+ callbackRefs.current.onLeave?.(id);
866
+ }
348
867
  }
868
+ prevSectionsInViewport.current = currentInViewport;
349
869
  }
350
- prevSectionsInViewport.current = currentInViewport;
870
+ const triggerLine = Math.round(effectiveOffset + scrollProgress * (viewportHeight - effectiveOffset));
351
871
  const newScrollState = {
352
- y: scrollY,
872
+ y: Math.round(scrollY),
353
873
  progress: Math.max(0, Math.min(1, scrollProgress)),
354
874
  direction: scrollDirection,
355
- velocity,
356
- isScrolling: isScrollingRef.current,
357
- maxScroll,
358
- viewportHeight,
359
- offset: effectiveOffset
875
+ velocity: Math.round(velocity),
876
+ scrolling: isScrollingRef.current,
877
+ maxScroll: Math.round(maxScroll),
878
+ viewportHeight: Math.round(viewportHeight),
879
+ trackingOffset: Math.round(effectiveOffset),
880
+ triggerLine
360
881
  };
361
882
  const newSections = {};
362
883
  for (const s of scores){
363
884
  newSections[s.id] = {
364
885
  bounds: {
365
- top: s.bounds.top,
366
- bottom: s.bounds.bottom,
367
- height: s.bounds.height
886
+ top: Math.round(s.bounds.top),
887
+ bottom: Math.round(s.bounds.bottom),
888
+ height: Math.round(s.bounds.height)
368
889
  },
369
890
  visibility: Math.round(s.visibilityRatio * 100) / 100,
370
891
  progress: Math.round(s.progress * 100) / 100,
371
- isInViewport: s.isInViewport,
372
- isActive: s.id === newActiveId
892
+ inView: s.inView,
893
+ active: s.id === (isProgrammatic ? currentActiveId : newActiveId),
894
+ rect: s.rect
373
895
  };
374
896
  }
375
897
  setScroll(newScrollState);
376
898
  setSections(newSections);
377
899
  }, [
378
- stableSectionIds,
900
+ sectionIds,
379
901
  sectionIndexMap,
380
- getEffectiveOffset,
381
- offsetRatio,
382
- visibilityThreshold,
383
- hysteresisMargin,
384
- getSectionBounds,
385
- containerElement
902
+ trackingOffset,
903
+ threshold,
904
+ hysteresis,
905
+ containerElement,
906
+ getCurrentSections
386
907
  ]);
387
908
  recalculateRef.current = calculateActiveSection;
388
909
  useEffect(()=>{
389
910
  const container = containerElement;
390
911
  const scrollTarget = container || window;
391
- const scheduleCalculate = ()=>{
392
- if (rafId.current) {
393
- cancelAnimationFrame(rafId.current);
394
- }
395
- rafId.current = requestAnimationFrame(()=>{
396
- rafId.current = null;
397
- calculateActiveSection();
398
- });
399
- };
400
912
  const handleScrollEnd = ()=>{
401
913
  isScrollingRef.current = false;
402
914
  setScroll((prev)=>({
403
915
  ...prev,
404
- isScrolling: false
916
+ scrolling: false
405
917
  }));
406
- callbackRefs.current.onScrollEnd == null ? void 0 : callbackRefs.current.onScrollEnd.call(callbackRefs.current);
918
+ callbackRefs.current.onScrollEnd?.();
407
919
  };
408
920
  const handleScroll = ()=>{
409
921
  if (!isScrollingRef.current) {
410
922
  isScrollingRef.current = true;
411
923
  setScroll((prev)=>({
412
924
  ...prev,
413
- isScrolling: true
925
+ scrolling: true
414
926
  }));
415
- callbackRefs.current.onScrollStart == null ? void 0 : callbackRefs.current.onScrollStart.call(callbackRefs.current);
927
+ callbackRefs.current.onScrollStart?.();
416
928
  }
417
929
  if (scrollIdleTimeoutRef.current) {
418
930
  clearTimeout(scrollIdleTimeoutRef.current);
@@ -427,7 +939,7 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
427
939
  if (throttleTimeoutId.current) {
428
940
  clearTimeout(throttleTimeoutId.current);
429
941
  }
430
- scheduleCalculate();
942
+ scheduleRecalculate();
431
943
  throttleTimeoutId.current = setTimeout(()=>{
432
944
  isThrottled.current = false;
433
945
  throttleTimeoutId.current = null;
@@ -435,14 +947,16 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
435
947
  hasPendingScroll.current = false;
436
948
  handleScroll();
437
949
  }
438
- }, debounceMs);
950
+ }, throttle);
439
951
  };
440
952
  const handleResize = ()=>{
441
- scheduleCalculate();
953
+ if (useSelector && selectorString) {
954
+ updateSectionsFromSelector(selectorString);
955
+ }
956
+ scheduleRecalculate();
442
957
  };
443
- calculateActiveSection();
444
958
  const deferredRecalcId = setTimeout(()=>{
445
- calculateActiveSection();
959
+ scheduleRecalculate();
446
960
  }, 0);
447
961
  scrollTarget.addEventListener("scroll", handleScroll, {
448
962
  passive: true
@@ -462,43 +976,43 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
462
976
  clearTimeout(throttleTimeoutId.current);
463
977
  throttleTimeoutId.current = null;
464
978
  }
465
- if (programmaticScrollTimeoutId.current) {
466
- clearTimeout(programmaticScrollTimeoutId.current);
467
- programmaticScrollTimeoutId.current = null;
468
- }
469
979
  if (scrollIdleTimeoutRef.current) {
470
980
  clearTimeout(scrollIdleTimeoutRef.current);
471
981
  scrollIdleTimeoutRef.current = null;
472
982
  }
473
- scrollCleanupRef.current == null ? void 0 : scrollCleanupRef.current.call(scrollCleanupRef);
983
+ scrollCleanupRef.current?.();
474
984
  isThrottled.current = false;
475
985
  hasPendingScroll.current = false;
476
986
  isProgrammaticScrolling.current = false;
477
987
  isScrollingRef.current = false;
478
988
  };
479
989
  }, [
480
- calculateActiveSection,
481
- debounceMs,
482
- containerElement
990
+ throttle,
991
+ containerElement,
992
+ useSelector,
993
+ selectorString,
994
+ updateSectionsFromSelector,
995
+ scheduleRecalculate
483
996
  ]);
484
- const activeIndex = useMemo(()=>{
485
- var _sectionIndexMap_get;
997
+ const index = useMemo(()=>{
486
998
  if (!activeId) return -1;
487
- return (_sectionIndexMap_get = sectionIndexMap.get(activeId)) != null ? _sectionIndexMap_get : -1;
999
+ return sectionIndexMap.get(activeId) ?? -1;
488
1000
  }, [
489
1001
  activeId,
490
1002
  sectionIndexMap
491
1003
  ]);
492
1004
  return {
493
- activeId,
494
- activeIndex,
1005
+ active: activeId,
1006
+ index,
1007
+ progress: scroll.progress,
1008
+ direction: scroll.direction,
495
1009
  scroll,
496
1010
  sections,
497
- registerRef,
498
- scrollToSection,
499
- sectionProps,
500
- navProps
1011
+ ids: sectionIds,
1012
+ scrollTo,
1013
+ register,
1014
+ link
501
1015
  };
502
1016
  }
503
1017
 
504
- export { useDomet as default, useDomet };
1018
+ export { VALIDATION_LIMITS, useDomet as default, useDomet };