domet 1.0.2 → 1.0.4

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 blksmr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/cjs/index.js CHANGED
@@ -1,16 +1,18 @@
1
- import { useMemo, useState, useRef, useCallback, useLayoutEffect, useEffect } from 'react';
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var react = require('react');
2
4
 
3
5
  const DEFAULT_VISIBILITY_THRESHOLD = 0.6;
4
6
  const DEFAULT_HYSTERESIS_MARGIN = 150;
5
7
  const SCROLL_IDLE_MS = 100;
6
- const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
8
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? react.useLayoutEffect : react.useEffect;
7
9
  function useDomet(sectionIds, containerRef = null, options = {}) {
8
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;
9
11
  JSON.stringify(sectionIds);
10
- const stableSectionIds = useMemo(()=>sectionIds, [
12
+ const stableSectionIds = react.useMemo(()=>sectionIds, [
11
13
  sectionIds
12
14
  ]);
13
- const sectionIndexMap = useMemo(()=>{
15
+ const sectionIndexMap = react.useMemo(()=>{
14
16
  const map = new Map();
15
17
  for(let i = 0; i < stableSectionIds.length; i++){
16
18
  map.set(stableSectionIds[i], i);
@@ -19,8 +21,8 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
19
21
  }, [
20
22
  stableSectionIds
21
23
  ]);
22
- const [activeId, setActiveId] = useState(stableSectionIds[0] || null);
23
- const [scroll, setScroll] = useState({
24
+ const [activeId, setActiveId] = react.useState(stableSectionIds[0] || null);
25
+ const [scroll, setScroll] = react.useState({
24
26
  y: 0,
25
27
  progress: 0,
26
28
  direction: null,
@@ -30,25 +32,25 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
30
32
  viewportHeight: 0,
31
33
  offset: 0
32
34
  });
33
- const [sections, setSections] = useState({});
34
- const [containerElement, setContainerElement] = useState(null);
35
- const refs = useRef({});
36
- const refCallbacks = useRef({});
37
- const activeIdRef = useRef(stableSectionIds[0] || null);
38
- const lastScrollY = useRef(0);
39
- const lastScrollTime = useRef(Date.now());
40
- const rafId = useRef(null);
41
- const isThrottled = useRef(false);
42
- const throttleTimeoutId = useRef(null);
43
- const hasPendingScroll = useRef(false);
44
- const isProgrammaticScrolling = useRef(false);
45
- const programmaticScrollTimeoutId = useRef(null);
46
- const isScrollingRef = useRef(false);
47
- const scrollIdleTimeoutRef = useRef(null);
48
- const prevSectionsInViewport = useRef(new Set());
49
- const recalculateRef = useRef(()=>{});
50
- const scrollCleanupRef = useRef(null);
51
- const callbackRefs = useRef({
35
+ const [sections, setSections] = react.useState({});
36
+ const [containerElement, setContainerElement] = react.useState(null);
37
+ const refs = react.useRef({});
38
+ const refCallbacks = react.useRef({});
39
+ const activeIdRef = react.useRef(stableSectionIds[0] || null);
40
+ const lastScrollY = react.useRef(0);
41
+ const lastScrollTime = react.useRef(Date.now());
42
+ const rafId = react.useRef(null);
43
+ const isThrottled = react.useRef(false);
44
+ const throttleTimeoutId = react.useRef(null);
45
+ const hasPendingScroll = react.useRef(false);
46
+ const isProgrammaticScrolling = react.useRef(false);
47
+ const programmaticScrollTimeoutId = react.useRef(null);
48
+ const isScrollingRef = react.useRef(false);
49
+ const scrollIdleTimeoutRef = react.useRef(null);
50
+ const prevSectionsInViewport = react.useRef(new Set());
51
+ const recalculateRef = react.useRef(()=>{});
52
+ const scrollCleanupRef = react.useRef(null);
53
+ const callbackRefs = react.useRef({
52
54
  onActiveChange,
53
55
  onSectionEnter,
54
56
  onSectionLeave,
@@ -62,12 +64,12 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
62
64
  onScrollStart,
63
65
  onScrollEnd
64
66
  };
65
- const getEffectiveOffset = useCallback(()=>{
67
+ const getEffectiveOffset = react.useCallback(()=>{
66
68
  return offset;
67
69
  }, [
68
70
  offset
69
71
  ]);
70
- const getScrollBehavior = useCallback(()=>{
72
+ const getScrollBehavior = react.useCallback(()=>{
71
73
  if (behavior === "auto") {
72
74
  if (typeof window === "undefined") return "instant";
73
75
  const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
@@ -87,7 +89,7 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
87
89
  containerRef,
88
90
  containerElement
89
91
  ]);
90
- const registerRef = useCallback((id)=>{
92
+ const registerRef = react.useCallback((id)=>{
91
93
  const existing = refCallbacks.current[id];
92
94
  if (existing) return existing;
93
95
  const callback = (el)=>{
@@ -100,7 +102,7 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
100
102
  refCallbacks.current[id] = callback;
101
103
  return callback;
102
104
  }, []);
103
- const scrollToSection = useCallback((id)=>{
105
+ const scrollToSection = react.useCallback((id)=>{
104
106
  if (!stableSectionIds.includes(id)) {
105
107
  if (process.env.NODE_ENV !== "production") {
106
108
  console.warn(`[domet] scrollToSection: id "${id}" not in sectionIds`);
@@ -196,14 +198,14 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
196
198
  getEffectiveOffset,
197
199
  getScrollBehavior
198
200
  ]);
199
- const sectionProps = useCallback((id)=>({
201
+ const sectionProps = react.useCallback((id)=>({
200
202
  id,
201
203
  ref: registerRef(id),
202
204
  "data-domet": id
203
205
  }), [
204
206
  registerRef
205
207
  ]);
206
- const navProps = useCallback((id)=>({
208
+ const navProps = react.useCallback((id)=>({
207
209
  onClick: ()=>scrollToSection(id),
208
210
  "aria-current": activeId === id ? "page" : undefined,
209
211
  "data-active": activeId === id
@@ -211,7 +213,7 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
211
213
  activeId,
212
214
  scrollToSection
213
215
  ]);
214
- useEffect(()=>{
216
+ react.useEffect(()=>{
215
217
  var _stableSectionIds_;
216
218
  const idsSet = new Set(stableSectionIds);
217
219
  for (const id of Object.keys(refs.current)){
@@ -233,7 +235,7 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
233
235
  }, [
234
236
  stableSectionIds
235
237
  ]);
236
- const getSectionBounds = useCallback(()=>{
238
+ const getSectionBounds = react.useCallback(()=>{
237
239
  const container = containerElement;
238
240
  const scrollTop = container ? container.scrollTop : window.scrollY;
239
241
  const containerTop = container ? container.getBoundingClientRect().top : 0;
@@ -253,7 +255,7 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
253
255
  stableSectionIds,
254
256
  containerElement
255
257
  ]);
256
- const calculateActiveSection = useCallback(()=>{
258
+ const calculateActiveSection = react.useCallback(()=>{
257
259
  if (isProgrammaticScrolling.current) return;
258
260
  const container = containerElement;
259
261
  const currentActiveId = activeIdRef.current;
@@ -385,7 +387,7 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
385
387
  containerElement
386
388
  ]);
387
389
  recalculateRef.current = calculateActiveSection;
388
- useEffect(()=>{
390
+ react.useEffect(()=>{
389
391
  const container = containerElement;
390
392
  const scrollTarget = container || window;
391
393
  const scheduleCalculate = ()=>{
@@ -481,7 +483,7 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
481
483
  debounceMs,
482
484
  containerElement
483
485
  ]);
484
- const activeIndex = useMemo(()=>{
486
+ const activeIndex = react.useMemo(()=>{
485
487
  var _sectionIndexMap_get;
486
488
  if (!activeId) return -1;
487
489
  return (_sectionIndexMap_get = sectionIndexMap.get(activeId)) != null ? _sectionIndexMap_get : -1;
@@ -501,4 +503,5 @@ function useDomet(sectionIds, containerRef = null, options = {}) {
501
503
  };
502
504
  }
503
505
 
504
- export { useDomet as default, useDomet };
506
+ exports.default = useDomet;
507
+ exports.useDomet = useDomet;
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "domet",
3
- "version": "1.0.2",
4
3
  "description": "A React hook for scroll tracking with smooth 60fps performance and smart hysteresis",
5
- "license": "MIT",
4
+ "version": "1.0.4",
6
5
  "author": "blksmr",
7
6
  "repository": {
8
7
  "type": "git",
@@ -21,10 +20,9 @@
21
20
  "navigation",
22
21
  "active-section"
23
22
  ],
24
- "type": "module",
25
- "main": "./dist/es/index.js",
26
- "module": "./dist/es/index.js",
27
- "types": "./dist/es/index.d.ts",
23
+ "main": "./dist/cjs/index.js",
24
+ "module": "./dist/es/index.mjs",
25
+ "types": "./dist/cjs/index.d.ts",
28
26
  "exports": {
29
27
  ".": {
30
28
  "import": {
@@ -1,62 +0,0 @@
1
- import { RefObject } from 'react';
2
-
3
- type SectionBounds = {
4
- top: number;
5
- bottom: number;
6
- height: number;
7
- };
8
- type ScrollState = {
9
- y: number;
10
- progress: number;
11
- direction: "up" | "down" | null;
12
- velocity: number;
13
- isScrolling: boolean;
14
- maxScroll: number;
15
- viewportHeight: number;
16
- offset: number;
17
- };
18
- type SectionState = {
19
- bounds: SectionBounds;
20
- visibility: number;
21
- progress: number;
22
- isInViewport: boolean;
23
- isActive: boolean;
24
- };
25
- type ScrollBehavior = "smooth" | "instant" | "auto";
26
- type DometOptions = {
27
- offset?: number;
28
- offsetRatio?: number;
29
- debounceMs?: number;
30
- visibilityThreshold?: number;
31
- hysteresisMargin?: number;
32
- behavior?: ScrollBehavior;
33
- onActiveChange?: (id: string | null, prevId: string | null) => void;
34
- onSectionEnter?: (id: string) => void;
35
- onSectionLeave?: (id: string) => void;
36
- onScrollStart?: () => void;
37
- onScrollEnd?: () => void;
38
- };
39
- type SectionProps = {
40
- id: string;
41
- ref: (el: HTMLElement | null) => void;
42
- "data-domet": string;
43
- };
44
- type NavProps = {
45
- onClick: () => void;
46
- "aria-current": "page" | undefined;
47
- "data-active": boolean;
48
- };
49
- type UseDometReturn = {
50
- activeId: string | null;
51
- activeIndex: number;
52
- scroll: ScrollState;
53
- sections: Record<string, SectionState>;
54
- registerRef: (id: string) => (el: HTMLElement | null) => void;
55
- scrollToSection: (id: string) => void;
56
- sectionProps: (id: string) => SectionProps;
57
- navProps: (id: string) => NavProps;
58
- };
59
- declare function useDomet(sectionIds: string[], containerRef?: RefObject<HTMLElement> | null, options?: DometOptions): UseDometReturn;
60
-
61
- export { useDomet as default, useDomet };
62
- export type { DometOptions, NavProps, ScrollBehavior, ScrollState, SectionBounds, SectionProps, SectionState, UseDometReturn };
package/dist/es/index.js DELETED
@@ -1,504 +0,0 @@
1
- import { useMemo, useState, useRef, useCallback, useLayoutEffect, useEffect } from 'react';
2
-
3
- const DEFAULT_VISIBILITY_THRESHOLD = 0.6;
4
- const DEFAULT_HYSTERESIS_MARGIN = 150;
5
- const SCROLL_IDLE_MS = 100;
6
- 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
12
- ]);
13
- const sectionIndexMap = useMemo(()=>{
14
- const map = new Map();
15
- for(let i = 0; i < stableSectionIds.length; i++){
16
- map.set(stableSectionIds[i], i);
17
- }
18
- return map;
19
- }, [
20
- stableSectionIds
21
- ]);
22
- const [activeId, setActiveId] = useState(stableSectionIds[0] || null);
23
- const [scroll, setScroll] = useState({
24
- y: 0,
25
- progress: 0,
26
- direction: null,
27
- velocity: 0,
28
- isScrolling: false,
29
- maxScroll: 0,
30
- viewportHeight: 0,
31
- offset: 0
32
- });
33
- const [sections, setSections] = useState({});
34
- const [containerElement, setContainerElement] = useState(null);
35
- const refs = useRef({});
36
- const refCallbacks = useRef({});
37
- const activeIdRef = useRef(stableSectionIds[0] || null);
38
- const lastScrollY = useRef(0);
39
- const lastScrollTime = useRef(Date.now());
40
- const rafId = useRef(null);
41
- const isThrottled = useRef(false);
42
- const throttleTimeoutId = useRef(null);
43
- const hasPendingScroll = useRef(false);
44
- const isProgrammaticScrolling = useRef(false);
45
- const programmaticScrollTimeoutId = useRef(null);
46
- const isScrollingRef = useRef(false);
47
- const scrollIdleTimeoutRef = useRef(null);
48
- const prevSectionsInViewport = useRef(new Set());
49
- const recalculateRef = useRef(()=>{});
50
- const scrollCleanupRef = useRef(null);
51
- const callbackRefs = useRef({
52
- onActiveChange,
53
- onSectionEnter,
54
- onSectionLeave,
55
- onScrollStart,
56
- onScrollEnd
57
- });
58
- callbackRefs.current = {
59
- onActiveChange,
60
- onSectionEnter,
61
- onSectionLeave,
62
- onScrollStart,
63
- onScrollEnd
64
- };
65
- const getEffectiveOffset = useCallback(()=>{
66
- return offset;
67
- }, [
68
- offset
69
- ]);
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";
75
- }
76
- return behavior;
77
- }, [
78
- behavior
79
- ]);
80
- useIsomorphicLayoutEffect(()=>{
81
- var _ref;
82
- const nextContainer = (_ref = containerRef == null ? void 0 : containerRef.current) != null ? _ref : null;
83
- if (nextContainer !== containerElement) {
84
- setContainerElement(nextContainer);
85
- }
86
- }, [
87
- containerRef,
88
- containerElement
89
- ]);
90
- const registerRef = useCallback((id)=>{
91
- const existing = refCallbacks.current[id];
92
- if (existing) return existing;
93
- const callback = (el)=>{
94
- if (el) {
95
- refs.current[id] = el;
96
- } else {
97
- delete refs.current[id];
98
- }
99
- };
100
- refCallbacks.current[id] = callback;
101
- 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`);
107
- }
108
- return;
109
- }
110
- const element = refs.current[id];
111
- if (!element) return;
112
- if (programmaticScrollTimeoutId.current) {
113
- clearTimeout(programmaticScrollTimeoutId.current);
114
- }
115
- scrollCleanupRef.current == null ? void 0 : scrollCleanupRef.current.call(scrollCleanupRef);
116
- isProgrammaticScrolling.current = true;
117
- activeIdRef.current = id;
118
- setActiveId(id);
119
- const container = containerElement;
120
- const elementRect = element.getBoundingClientRect();
121
- const effectiveOffset = getEffectiveOffset() + 10;
122
- const scrollTarget = container || window;
123
- const unlockScroll = ()=>{
124
- isProgrammaticScrolling.current = false;
125
- if (programmaticScrollTimeoutId.current) {
126
- clearTimeout(programmaticScrollTimeoutId.current);
127
- programmaticScrollTimeoutId.current = null;
128
- }
129
- requestAnimationFrame(()=>{
130
- recalculateRef.current();
131
- });
132
- };
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);
141
- if ("onscrollend" in scrollTarget) {
142
- scrollTarget.removeEventListener("scrollend", handleScrollEnd);
143
- }
144
- scrollCleanupRef.current = null;
145
- };
146
- const doUnlock = ()=>{
147
- if (isUnlocked) return;
148
- isUnlocked = true;
149
- cleanup();
150
- unlockScroll();
151
- };
152
- const resetDebounce = ()=>{
153
- if (debounceTimer) {
154
- clearTimeout(debounceTimer);
155
- }
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
- });
171
- }
172
- scrollCleanupRef.current = cleanup;
173
- const scrollBehavior = getScrollBehavior();
174
- if (container) {
175
- const containerRect = container.getBoundingClientRect();
176
- const relativeTop = elementRect.top - containerRect.top + container.scrollTop;
177
- container.scrollTo({
178
- top: relativeTop - effectiveOffset,
179
- behavior: scrollBehavior
180
- });
181
- } else {
182
- const absoluteTop = elementRect.top + window.scrollY;
183
- window.scrollTo({
184
- top: absoluteTop - effectiveOffset,
185
- behavior: scrollBehavior
186
- });
187
- }
188
- if (scrollBehavior === "instant") {
189
- doUnlock();
190
- } else {
191
- resetDebounce();
192
- }
193
- }, [
194
- stableSectionIds,
195
- containerElement,
196
- getEffectiveOffset,
197
- getScrollBehavior
198
- ]);
199
- const sectionProps = useCallback((id)=>({
200
- id,
201
- ref: registerRef(id),
202
- "data-domet": id
203
- }), [
204
- registerRef
205
- ]);
206
- const navProps = useCallback((id)=>({
207
- onClick: ()=>scrollToSection(id),
208
- "aria-current": activeId === id ? "page" : undefined,
209
- "data-active": activeId === id
210
- }), [
211
- 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
255
- ]);
256
- const calculateActiveSection = useCallback(()=>{
257
- if (isProgrammaticScrolling.current) return;
258
- const container = containerElement;
259
- const currentActiveId = activeIdRef.current;
260
- const now = Date.now();
261
- const scrollY = container ? container.scrollTop : window.scrollY;
262
- const viewportHeight = container ? container.clientHeight : window.innerHeight;
263
- const scrollHeight = container ? container.scrollHeight : document.documentElement.scrollHeight;
264
- const maxScroll = Math.max(0, scrollHeight - viewportHeight);
265
- const scrollProgress = maxScroll > 0 ? scrollY / maxScroll : 0;
266
- const scrollDirection = scrollY === lastScrollY.current ? null : scrollY > lastScrollY.current ? "down" : "up";
267
- const deltaTime = now - lastScrollTime.current;
268
- const deltaY = scrollY - lastScrollY.current;
269
- const velocity = deltaTime > 0 ? Math.abs(deltaY) / deltaTime : 0;
270
- lastScrollY.current = scrollY;
271
- lastScrollTime.current = now;
272
- const sectionBounds = getSectionBounds();
273
- 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) {
334
- activeIdRef.current = newActiveId;
335
- setActiveId(newActiveId);
336
- callbackRefs.current.onActiveChange == null ? void 0 : callbackRefs.current.onActiveChange.call(callbackRefs.current, newActiveId, currentActiveId);
337
- }
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);
343
- }
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);
348
- }
349
- }
350
- prevSectionsInViewport.current = currentInViewport;
351
- const newScrollState = {
352
- y: scrollY,
353
- progress: Math.max(0, Math.min(1, scrollProgress)),
354
- direction: scrollDirection,
355
- velocity,
356
- isScrolling: isScrollingRef.current,
357
- maxScroll,
358
- viewportHeight,
359
- offset: effectiveOffset
360
- };
361
- const newSections = {};
362
- for (const s of scores){
363
- newSections[s.id] = {
364
- bounds: {
365
- top: s.bounds.top,
366
- bottom: s.bounds.bottom,
367
- height: s.bounds.height
368
- },
369
- visibility: Math.round(s.visibilityRatio * 100) / 100,
370
- progress: Math.round(s.progress * 100) / 100,
371
- isInViewport: s.isInViewport,
372
- isActive: s.id === newActiveId
373
- };
374
- }
375
- setScroll(newScrollState);
376
- setSections(newSections);
377
- }, [
378
- stableSectionIds,
379
- sectionIndexMap,
380
- getEffectiveOffset,
381
- offsetRatio,
382
- visibilityThreshold,
383
- hysteresisMargin,
384
- getSectionBounds,
385
- containerElement
386
- ]);
387
- recalculateRef.current = calculateActiveSection;
388
- useEffect(()=>{
389
- const container = containerElement;
390
- 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
- const handleScrollEnd = ()=>{
401
- isScrollingRef.current = false;
402
- setScroll((prev)=>({
403
- ...prev,
404
- isScrolling: false
405
- }));
406
- callbackRefs.current.onScrollEnd == null ? void 0 : callbackRefs.current.onScrollEnd.call(callbackRefs.current);
407
- };
408
- const handleScroll = ()=>{
409
- if (!isScrollingRef.current) {
410
- isScrollingRef.current = true;
411
- setScroll((prev)=>({
412
- ...prev,
413
- isScrolling: true
414
- }));
415
- callbackRefs.current.onScrollStart == null ? void 0 : callbackRefs.current.onScrollStart.call(callbackRefs.current);
416
- }
417
- if (scrollIdleTimeoutRef.current) {
418
- clearTimeout(scrollIdleTimeoutRef.current);
419
- }
420
- scrollIdleTimeoutRef.current = setTimeout(handleScrollEnd, SCROLL_IDLE_MS);
421
- if (isThrottled.current) {
422
- hasPendingScroll.current = true;
423
- return;
424
- }
425
- isThrottled.current = true;
426
- hasPendingScroll.current = false;
427
- if (throttleTimeoutId.current) {
428
- clearTimeout(throttleTimeoutId.current);
429
- }
430
- scheduleCalculate();
431
- throttleTimeoutId.current = setTimeout(()=>{
432
- isThrottled.current = false;
433
- throttleTimeoutId.current = null;
434
- if (hasPendingScroll.current) {
435
- hasPendingScroll.current = false;
436
- handleScroll();
437
- }
438
- }, debounceMs);
439
- };
440
- const handleResize = ()=>{
441
- scheduleCalculate();
442
- };
443
- calculateActiveSection();
444
- const deferredRecalcId = setTimeout(()=>{
445
- calculateActiveSection();
446
- }, 0);
447
- scrollTarget.addEventListener("scroll", handleScroll, {
448
- passive: true
449
- });
450
- window.addEventListener("resize", handleResize, {
451
- passive: true
452
- });
453
- return ()=>{
454
- clearTimeout(deferredRecalcId);
455
- scrollTarget.removeEventListener("scroll", handleScroll);
456
- window.removeEventListener("resize", handleResize);
457
- if (rafId.current) {
458
- cancelAnimationFrame(rafId.current);
459
- rafId.current = null;
460
- }
461
- if (throttleTimeoutId.current) {
462
- clearTimeout(throttleTimeoutId.current);
463
- throttleTimeoutId.current = null;
464
- }
465
- if (programmaticScrollTimeoutId.current) {
466
- clearTimeout(programmaticScrollTimeoutId.current);
467
- programmaticScrollTimeoutId.current = null;
468
- }
469
- if (scrollIdleTimeoutRef.current) {
470
- clearTimeout(scrollIdleTimeoutRef.current);
471
- scrollIdleTimeoutRef.current = null;
472
- }
473
- scrollCleanupRef.current == null ? void 0 : scrollCleanupRef.current.call(scrollCleanupRef);
474
- isThrottled.current = false;
475
- hasPendingScroll.current = false;
476
- isProgrammaticScrolling.current = false;
477
- isScrollingRef.current = false;
478
- };
479
- }, [
480
- calculateActiveSection,
481
- debounceMs,
482
- containerElement
483
- ]);
484
- const activeIndex = useMemo(()=>{
485
- var _sectionIndexMap_get;
486
- if (!activeId) return -1;
487
- return (_sectionIndexMap_get = sectionIndexMap.get(activeId)) != null ? _sectionIndexMap_get : -1;
488
- }, [
489
- activeId,
490
- sectionIndexMap
491
- ]);
492
- return {
493
- activeId,
494
- activeIndex,
495
- scroll,
496
- sections,
497
- registerRef,
498
- scrollToSection,
499
- sectionProps,
500
- navProps
501
- };
502
- }
503
-
504
- export { useDomet as default, useDomet };