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