@dreamstack-us/section-flow 0.0.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/index.cjs ADDED
@@ -0,0 +1,2281 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ const react = __toESM(require("react"));
25
+ const react_native = __toESM(require("react-native"));
26
+ const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
27
+
28
+ //#region src/constants.ts
29
+ /**
30
+ * Default configuration values for SectionFlow
31
+ */
32
+ const DEFAULT_ESTIMATED_ITEM_SIZE = 50;
33
+ const DEFAULT_ESTIMATED_HEADER_SIZE = 40;
34
+ const DEFAULT_ESTIMATED_FOOTER_SIZE = 0;
35
+ const DEFAULT_DRAW_DISTANCE = 250;
36
+ const DEFAULT_MAX_POOL_SIZE = 10;
37
+ const DEFAULT_ITEM_TYPE = "default";
38
+ const SECTION_HEADER_TYPE = "__section_header__";
39
+ const SECTION_FOOTER_TYPE = "__section_footer__";
40
+ const DEFAULT_VIEWABILITY_CONFIG = {
41
+ minimumViewTime: 250,
42
+ viewAreaCoveragePercentThreshold: 0,
43
+ itemVisiblePercentThreshold: 50,
44
+ waitForInteraction: false
45
+ };
46
+ const SCROLL_VELOCITY_THRESHOLD = 2;
47
+
48
+ //#endregion
49
+ //#region src/core/LayoutCache.ts
50
+ var LayoutCacheImpl = class {
51
+ cache = new Map();
52
+ typeStats = new Map();
53
+ get(key) {
54
+ return this.cache.get(key);
55
+ }
56
+ set(key, layout) {
57
+ this.cache.set(key, layout);
58
+ }
59
+ has(key) {
60
+ return this.cache.has(key);
61
+ }
62
+ delete(key) {
63
+ this.cache.delete(key);
64
+ }
65
+ clear() {
66
+ this.cache.clear();
67
+ this.typeStats.clear();
68
+ }
69
+ /**
70
+ * Invalidate all cached layouts from a given flat index onwards.
71
+ * Used when items are inserted/removed and positions need recalculation.
72
+ */
73
+ invalidateFrom(flatIndex, keyToIndexMap) {
74
+ for (const [key, index] of keyToIndexMap) if (index >= flatIndex) this.cache.delete(key);
75
+ }
76
+ /**
77
+ * Get the average measured size for items of a given type.
78
+ * Used for predicting unmeasured item sizes.
79
+ */
80
+ getAverageSize(itemType) {
81
+ const stats = this.typeStats.get(itemType);
82
+ if (!stats || stats.count === 0) return void 0;
83
+ return stats.totalSize / stats.count;
84
+ }
85
+ /**
86
+ * Record a measurement for computing type averages.
87
+ */
88
+ recordMeasurement(itemType, size) {
89
+ let stats = this.typeStats.get(itemType);
90
+ if (!stats) {
91
+ stats = {
92
+ totalSize: 0,
93
+ count: 0
94
+ };
95
+ this.typeStats.set(itemType, stats);
96
+ }
97
+ stats.totalSize += size;
98
+ stats.count += 1;
99
+ }
100
+ get size() {
101
+ return this.cache.size;
102
+ }
103
+ };
104
+ /**
105
+ * Factory function to create a LayoutCache instance.
106
+ */
107
+ function createLayoutCache() {
108
+ return new LayoutCacheImpl();
109
+ }
110
+
111
+ //#endregion
112
+ //#region src/core/LinearLayoutPositioner.ts
113
+ /**
114
+ * LinearLayoutPositioner implements the LayoutPositioner interface for
115
+ * standard vertical or horizontal list layouts.
116
+ *
117
+ * It computes absolute positions for each item based on:
118
+ * 1. Measured sizes from LayoutCache (when available)
119
+ * 2. Estimated sizes (when not yet measured)
120
+ * 3. Type-specific size predictions
121
+ */
122
+ var LinearLayoutPositioner = class {
123
+ flattenedData = [];
124
+ computedLayouts = new Map();
125
+ totalContentSize = {
126
+ width: 0,
127
+ height: 0
128
+ };
129
+ layoutsValid = false;
130
+ constructor(layoutCache, getItemType, options = {}) {
131
+ this.layoutCache = layoutCache;
132
+ this.getItemType = getItemType;
133
+ this.horizontal = options.horizontal ?? false;
134
+ this.estimatedItemSize = options.estimatedItemSize ?? DEFAULT_ESTIMATED_ITEM_SIZE;
135
+ this.estimatedHeaderSize = options.estimatedHeaderSize ?? DEFAULT_ESTIMATED_HEADER_SIZE;
136
+ this.estimatedFooterSize = options.estimatedFooterSize ?? DEFAULT_ESTIMATED_FOOTER_SIZE;
137
+ this.containerWidth = options.containerWidth ?? 0;
138
+ this.containerHeight = options.containerHeight ?? 0;
139
+ }
140
+ /**
141
+ * Update the flattened data and invalidate layouts.
142
+ */
143
+ setData(flattenedData) {
144
+ this.flattenedData = flattenedData;
145
+ this.invalidateAll();
146
+ }
147
+ /**
148
+ * Update container dimensions.
149
+ */
150
+ setContainerSize(width, height) {
151
+ if (this.containerWidth !== width || this.containerHeight !== height) {
152
+ this.containerWidth = width;
153
+ this.containerHeight = height;
154
+ this.invalidateAll();
155
+ }
156
+ }
157
+ /**
158
+ * Get the estimated size for an item based on its type.
159
+ */
160
+ getEstimatedSize(flatIndex) {
161
+ const item = this.flattenedData[flatIndex];
162
+ if (!item) return this.estimatedItemSize;
163
+ const itemType = this.getItemType(flatIndex);
164
+ const avgSize = this.layoutCache.getAverageSize(itemType);
165
+ if (avgSize !== void 0) return avgSize;
166
+ if (item.type === "section-header") return this.estimatedHeaderSize;
167
+ if (item.type === "section-footer") return this.estimatedFooterSize;
168
+ return this.estimatedItemSize;
169
+ }
170
+ /**
171
+ * Get the actual or estimated size for an item.
172
+ */
173
+ getItemSize(flatIndex) {
174
+ const item = this.flattenedData[flatIndex];
175
+ if (!item) return this.estimatedItemSize;
176
+ const cached = this.layoutCache.get(item.key);
177
+ if (cached) return this.horizontal ? cached.width : cached.height;
178
+ return this.getEstimatedSize(flatIndex);
179
+ }
180
+ /**
181
+ * Compute layouts for all items if not already computed.
182
+ */
183
+ ensureLayoutsComputed() {
184
+ if (this.layoutsValid) return;
185
+ this.computedLayouts.clear();
186
+ let offset = 0;
187
+ const crossAxisSize = this.horizontal ? this.containerHeight : this.containerWidth;
188
+ for (let i = 0; i < this.flattenedData.length; i++) {
189
+ const size = this.getItemSize(i);
190
+ const layout = this.horizontal ? {
191
+ x: offset,
192
+ y: 0,
193
+ width: size,
194
+ height: crossAxisSize
195
+ } : {
196
+ x: 0,
197
+ y: offset,
198
+ width: crossAxisSize,
199
+ height: size
200
+ };
201
+ this.computedLayouts.set(i, layout);
202
+ offset += size;
203
+ }
204
+ this.totalContentSize = this.horizontal ? {
205
+ width: offset,
206
+ height: crossAxisSize
207
+ } : {
208
+ width: crossAxisSize,
209
+ height: offset
210
+ };
211
+ this.layoutsValid = true;
212
+ }
213
+ /**
214
+ * Get the layout for a specific index.
215
+ */
216
+ getLayoutForIndex(index) {
217
+ this.ensureLayoutsComputed();
218
+ return this.computedLayouts.get(index) ?? {
219
+ x: 0,
220
+ y: 0,
221
+ width: 0,
222
+ height: 0
223
+ };
224
+ }
225
+ /**
226
+ * Get the total content size.
227
+ */
228
+ getContentSize() {
229
+ this.ensureLayoutsComputed();
230
+ return this.totalContentSize;
231
+ }
232
+ /**
233
+ * Get the range of visible indices for the given scroll position.
234
+ * Uses binary search for efficiency with large lists.
235
+ */
236
+ getVisibleRange(scrollOffset, viewportSize, overscan) {
237
+ this.ensureLayoutsComputed();
238
+ if (this.flattenedData.length === 0) return {
239
+ startIndex: 0,
240
+ endIndex: -1
241
+ };
242
+ const startOffset = Math.max(0, scrollOffset - overscan);
243
+ const endOffset = scrollOffset + viewportSize + overscan;
244
+ const startIndex = this.binarySearchStart(startOffset);
245
+ const endIndex = this.binarySearchEnd(endOffset);
246
+ return {
247
+ startIndex: Math.max(0, startIndex),
248
+ endIndex: Math.min(this.flattenedData.length - 1, endIndex)
249
+ };
250
+ }
251
+ /**
252
+ * Binary search to find first item that ends after the given offset.
253
+ */
254
+ binarySearchStart(offset) {
255
+ let low = 0;
256
+ let high = this.flattenedData.length - 1;
257
+ while (low < high) {
258
+ const mid = Math.floor((low + high) / 2);
259
+ const layout = this.computedLayouts.get(mid);
260
+ if (!layout) {
261
+ low = mid + 1;
262
+ continue;
263
+ }
264
+ const itemEnd = this.horizontal ? layout.x + layout.width : layout.y + layout.height;
265
+ if (itemEnd <= offset) low = mid + 1;
266
+ else high = mid;
267
+ }
268
+ return low;
269
+ }
270
+ /**
271
+ * Binary search to find last item that starts before the given offset.
272
+ */
273
+ binarySearchEnd(offset) {
274
+ let low = 0;
275
+ let high = this.flattenedData.length - 1;
276
+ while (low < high) {
277
+ const mid = Math.ceil((low + high) / 2);
278
+ const layout = this.computedLayouts.get(mid);
279
+ if (!layout) {
280
+ high = mid - 1;
281
+ continue;
282
+ }
283
+ const itemStart = this.horizontal ? layout.x : layout.y;
284
+ if (itemStart >= offset) high = mid - 1;
285
+ else low = mid;
286
+ }
287
+ return high;
288
+ }
289
+ /**
290
+ * Update the layout for a specific index after measurement.
291
+ */
292
+ updateItemLayout(index, layout) {
293
+ const item = this.flattenedData[index];
294
+ if (!item) return;
295
+ const itemType = this.getItemType(index);
296
+ const size = this.horizontal ? layout.width : layout.height;
297
+ this.layoutCache.recordMeasurement(itemType, size);
298
+ this.layoutCache.set(item.key, layout);
299
+ this.invalidateFrom(index);
300
+ }
301
+ /**
302
+ * Invalidate layouts from a given index.
303
+ */
304
+ invalidateFrom(index) {
305
+ for (let i = index; i < this.flattenedData.length; i++) this.computedLayouts.delete(i);
306
+ this.layoutsValid = false;
307
+ }
308
+ /**
309
+ * Invalidate all layouts.
310
+ */
311
+ invalidateAll() {
312
+ this.computedLayouts.clear();
313
+ this.layoutsValid = false;
314
+ }
315
+ /**
316
+ * Get the index of the item at or just before the given offset.
317
+ */
318
+ getIndexForOffset(offset) {
319
+ this.ensureLayoutsComputed();
320
+ if (this.flattenedData.length === 0) return 0;
321
+ return this.binarySearchStart(offset);
322
+ }
323
+ /**
324
+ * Get the total number of items.
325
+ */
326
+ getTotalItemCount() {
327
+ return this.flattenedData.length;
328
+ }
329
+ };
330
+ /**
331
+ * Factory function to create a LinearLayoutPositioner.
332
+ */
333
+ function createLayoutPositioner(layoutCache, getItemType, options) {
334
+ return new LinearLayoutPositioner(layoutCache, getItemType, options);
335
+ }
336
+
337
+ //#endregion
338
+ //#region src/core/SectionLayoutManager.ts
339
+ var SectionLayoutManagerImpl = class {
340
+ sections = [];
341
+ flattenedData = [];
342
+ collapsedSections = new Set();
343
+ sectionBoundaries = new Map();
344
+ flatIndexToSection = new Map();
345
+ constructor(layoutCache, options = {}) {
346
+ this.layoutCache = layoutCache;
347
+ this.horizontal = options.horizontal ?? false;
348
+ this.hasSectionFooters = options.hasSectionFooters ?? false;
349
+ this.layoutPositioner = new LinearLayoutPositioner(layoutCache, (flatIndex) => this.getItemTypeForIndex(flatIndex), {
350
+ horizontal: this.horizontal,
351
+ estimatedItemSize: options.estimatedItemSize,
352
+ estimatedHeaderSize: options.estimatedHeaderSize,
353
+ estimatedFooterSize: options.estimatedFooterSize
354
+ });
355
+ }
356
+ /**
357
+ * Get the item type for a flat index (used by layout positioner).
358
+ */
359
+ getItemTypeForIndex(flatIndex) {
360
+ const item = this.flattenedData[flatIndex];
361
+ if (!item) return "default";
362
+ if (item.type === "section-header") return "__section_header__";
363
+ if (item.type === "section-footer") return "__section_footer__";
364
+ return "default";
365
+ }
366
+ /**
367
+ * Update sections and compute flattened data.
368
+ */
369
+ updateData(sections, collapsedSections) {
370
+ this.sections = sections;
371
+ this.collapsedSections = collapsedSections;
372
+ this.flattenedData = [];
373
+ this.sectionBoundaries.clear();
374
+ this.flatIndexToSection.clear();
375
+ let flatIndex = 0;
376
+ for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
377
+ const section = sections[sectionIndex];
378
+ const isCollapsed = collapsedSections.has(section.key);
379
+ const boundary = {
380
+ sectionIndex,
381
+ sectionKey: section.key,
382
+ headerFlatIndex: flatIndex,
383
+ firstItemFlatIndex: -1,
384
+ lastItemFlatIndex: -1,
385
+ footerFlatIndex: null,
386
+ itemCount: 0
387
+ };
388
+ this.flattenedData.push({
389
+ type: "section-header",
390
+ key: `header-${section.key}`,
391
+ sectionKey: section.key,
392
+ sectionIndex,
393
+ itemIndex: -1,
394
+ item: null,
395
+ section
396
+ });
397
+ this.flatIndexToSection.set(flatIndex, boundary);
398
+ flatIndex++;
399
+ if (!isCollapsed) {
400
+ boundary.firstItemFlatIndex = flatIndex;
401
+ for (let itemIndex = 0; itemIndex < section.data.length; itemIndex++) {
402
+ this.flattenedData.push({
403
+ type: "item",
404
+ key: `item-${section.key}-${itemIndex}`,
405
+ sectionKey: section.key,
406
+ sectionIndex,
407
+ itemIndex,
408
+ item: section.data[itemIndex],
409
+ section
410
+ });
411
+ this.flatIndexToSection.set(flatIndex, boundary);
412
+ flatIndex++;
413
+ }
414
+ boundary.lastItemFlatIndex = flatIndex - 1;
415
+ boundary.itemCount = section.data.length;
416
+ if (this.hasSectionFooters) {
417
+ boundary.footerFlatIndex = flatIndex;
418
+ this.flattenedData.push({
419
+ type: "section-footer",
420
+ key: `footer-${section.key}`,
421
+ sectionKey: section.key,
422
+ sectionIndex,
423
+ itemIndex: -1,
424
+ item: null,
425
+ section
426
+ });
427
+ this.flatIndexToSection.set(flatIndex, boundary);
428
+ flatIndex++;
429
+ }
430
+ }
431
+ this.sectionBoundaries.set(section.key, boundary);
432
+ }
433
+ this.layoutPositioner.setData(this.flattenedData);
434
+ return this.flattenedData;
435
+ }
436
+ /**
437
+ * Get the flat index for a section/item coordinate.
438
+ */
439
+ getFlatIndex(sectionIndex, itemIndex) {
440
+ const section = this.sections[sectionIndex];
441
+ if (!section) return -1;
442
+ const boundary = this.sectionBoundaries.get(section.key);
443
+ if (!boundary) return -1;
444
+ if (itemIndex === -1) return boundary.headerFlatIndex;
445
+ if (this.collapsedSections.has(section.key)) return -1;
446
+ if (itemIndex < 0 || itemIndex >= section.data.length) return -1;
447
+ return boundary.firstItemFlatIndex + itemIndex;
448
+ }
449
+ /**
450
+ * Get section/item coordinates from a flat index.
451
+ */
452
+ getSectionItemIndex(flatIndex) {
453
+ const item = this.flattenedData[flatIndex];
454
+ if (!item) return {
455
+ sectionIndex: -1,
456
+ itemIndex: -1,
457
+ isHeader: false,
458
+ isFooter: false
459
+ };
460
+ return {
461
+ sectionIndex: item.sectionIndex,
462
+ itemIndex: item.itemIndex,
463
+ isHeader: item.type === "section-header",
464
+ isFooter: item.type === "section-footer"
465
+ };
466
+ }
467
+ /**
468
+ * Get layout info for a section.
469
+ */
470
+ getSectionLayout(sectionKey) {
471
+ const boundary = this.sectionBoundaries.get(sectionKey);
472
+ if (!boundary) return null;
473
+ const headerLayout = this.layoutPositioner.getLayoutForIndex(boundary.headerFlatIndex);
474
+ let footerLayout = null;
475
+ if (boundary.footerFlatIndex !== null) footerLayout = this.layoutPositioner.getLayoutForIndex(boundary.footerFlatIndex);
476
+ const isCollapsed = this.collapsedSections.has(sectionKey);
477
+ let itemsStartOffset = this.horizontal ? headerLayout.x + headerLayout.width : headerLayout.y + headerLayout.height;
478
+ let itemsEndOffset = itemsStartOffset;
479
+ if (!isCollapsed && boundary.lastItemFlatIndex >= boundary.firstItemFlatIndex) {
480
+ const lastItemLayout = this.layoutPositioner.getLayoutForIndex(boundary.lastItemFlatIndex);
481
+ itemsEndOffset = this.horizontal ? lastItemLayout.x + lastItemLayout.width : lastItemLayout.y + lastItemLayout.height;
482
+ }
483
+ return {
484
+ sectionKey,
485
+ sectionIndex: boundary.sectionIndex,
486
+ headerLayout,
487
+ footerLayout,
488
+ itemsStartOffset,
489
+ itemsEndOffset,
490
+ itemCount: boundary.itemCount,
491
+ isCollapsed
492
+ };
493
+ }
494
+ /**
495
+ * Get the section containing a given scroll offset.
496
+ */
497
+ getSectionAtOffset(offset) {
498
+ for (const [sectionKey] of this.sectionBoundaries) {
499
+ const layout = this.getSectionLayout(sectionKey);
500
+ if (!layout) continue;
501
+ const sectionStart = this.horizontal ? layout.headerLayout.x : layout.headerLayout.y;
502
+ const sectionEnd = layout.itemsEndOffset;
503
+ if (offset >= sectionStart && offset < sectionEnd) return layout;
504
+ }
505
+ return null;
506
+ }
507
+ /**
508
+ * Get layout info for all sections.
509
+ */
510
+ getAllSectionLayouts() {
511
+ const layouts = [];
512
+ for (const [sectionKey] of this.sectionBoundaries) {
513
+ const layout = this.getSectionLayout(sectionKey);
514
+ if (layout) layouts.push(layout);
515
+ }
516
+ return layouts;
517
+ }
518
+ /**
519
+ * Set a section's collapsed state.
520
+ */
521
+ setSectionCollapsed(sectionKey, collapsed) {
522
+ if (collapsed) this.collapsedSections.add(sectionKey);
523
+ else this.collapsedSections.delete(sectionKey);
524
+ this.updateData(this.sections, this.collapsedSections);
525
+ }
526
+ /**
527
+ * Check if a section is collapsed.
528
+ */
529
+ isSectionCollapsed(sectionKey) {
530
+ return this.collapsedSections.has(sectionKey);
531
+ }
532
+ /**
533
+ * Get all collapsed sections.
534
+ */
535
+ getCollapsedSections() {
536
+ return new Set(this.collapsedSections);
537
+ }
538
+ /**
539
+ * Get the layout positioner for direct layout operations.
540
+ */
541
+ getLayoutPositioner() {
542
+ return this.layoutPositioner;
543
+ }
544
+ };
545
+ /**
546
+ * Factory function to create a SectionLayoutManager.
547
+ */
548
+ function createSectionLayoutManager(layoutCache, options) {
549
+ return new SectionLayoutManagerImpl(layoutCache, options);
550
+ }
551
+
552
+ //#endregion
553
+ //#region src/hooks/useScrollHandler.ts
554
+ /**
555
+ * Hook for tracking scroll state with velocity and direction detection.
556
+ * Used for adaptive buffering and scroll optimization.
557
+ */
558
+ function useScrollHandler(options = {}) {
559
+ const { horizontal = false, onScrollStateChange, onEndReached, onEndReachedThreshold = .5 } = options;
560
+ const scrollState = (0, react.useRef)({
561
+ offset: 0,
562
+ velocity: 0,
563
+ direction: "idle",
564
+ isScrolling: false,
565
+ contentSize: 0,
566
+ viewportSize: 0
567
+ });
568
+ const lastOffset = (0, react.useRef)(0);
569
+ const lastTimestamp = (0, react.useRef)(Date.now());
570
+ const endReachedCalled = (0, react.useRef)(false);
571
+ /**
572
+ * Calculate velocity from consecutive scroll events.
573
+ */
574
+ const calculateVelocity = (0, react.useCallback)((newOffset) => {
575
+ const now = Date.now();
576
+ const timeDelta = now - lastTimestamp.current;
577
+ if (timeDelta === 0) return scrollState.current.velocity;
578
+ const offsetDelta = newOffset - lastOffset.current;
579
+ const velocity = offsetDelta / timeDelta;
580
+ lastOffset.current = newOffset;
581
+ lastTimestamp.current = now;
582
+ return velocity;
583
+ }, []);
584
+ /**
585
+ * Determine scroll direction from velocity.
586
+ */
587
+ const getDirection = (0, react.useCallback)((velocity) => {
588
+ if (Math.abs(velocity) < .1) return "idle";
589
+ return velocity > 0 ? "forward" : "backward";
590
+ }, []);
591
+ /**
592
+ * Check if we've reached the end and should trigger callback.
593
+ */
594
+ const checkEndReached = (0, react.useCallback)(() => {
595
+ if (!onEndReached) return;
596
+ const { offset, contentSize, viewportSize } = scrollState.current;
597
+ const distanceFromEnd = contentSize - offset - viewportSize;
598
+ const threshold = viewportSize * onEndReachedThreshold;
599
+ if (distanceFromEnd <= threshold && !endReachedCalled.current) {
600
+ endReachedCalled.current = true;
601
+ onEndReached(distanceFromEnd);
602
+ } else if (distanceFromEnd > threshold) endReachedCalled.current = false;
603
+ }, [onEndReached, onEndReachedThreshold]);
604
+ /**
605
+ * Update scroll state and notify listeners.
606
+ */
607
+ const updateScrollState = (0, react.useCallback)((event, isScrolling) => {
608
+ const { contentOffset, contentSize, layoutMeasurement } = event;
609
+ const offset = horizontal ? contentOffset.x : contentOffset.y;
610
+ const size = horizontal ? contentSize.width : contentSize.height;
611
+ const viewport = horizontal ? layoutMeasurement.width : layoutMeasurement.height;
612
+ const velocity = calculateVelocity(offset);
613
+ const direction = getDirection(velocity);
614
+ scrollState.current = {
615
+ offset,
616
+ velocity,
617
+ direction,
618
+ isScrolling,
619
+ contentSize: size,
620
+ viewportSize: viewport
621
+ };
622
+ onScrollStateChange?.(scrollState.current);
623
+ checkEndReached();
624
+ }, [
625
+ horizontal,
626
+ calculateVelocity,
627
+ getDirection,
628
+ onScrollStateChange,
629
+ checkEndReached
630
+ ]);
631
+ const onScroll = (0, react.useCallback)((event) => {
632
+ updateScrollState(event.nativeEvent, scrollState.current.isScrolling);
633
+ }, [updateScrollState]);
634
+ const onScrollBeginDrag = (0, react.useCallback)((event) => {
635
+ scrollState.current.isScrolling = true;
636
+ updateScrollState(event.nativeEvent, true);
637
+ }, [updateScrollState]);
638
+ const onScrollEndDrag = (0, react.useCallback)((event) => {
639
+ updateScrollState(event.nativeEvent, scrollState.current.isScrolling);
640
+ }, [updateScrollState]);
641
+ const onMomentumScrollBegin = (0, react.useCallback)((event) => {
642
+ scrollState.current.isScrolling = true;
643
+ updateScrollState(event.nativeEvent, true);
644
+ }, [updateScrollState]);
645
+ const onMomentumScrollEnd = (0, react.useCallback)((event) => {
646
+ scrollState.current.isScrolling = false;
647
+ scrollState.current.direction = "idle";
648
+ scrollState.current.velocity = 0;
649
+ updateScrollState(event.nativeEvent, false);
650
+ }, [updateScrollState]);
651
+ const onContentSizeChange = (0, react.useCallback)((width, height) => {
652
+ scrollState.current.contentSize = horizontal ? width : height;
653
+ }, [horizontal]);
654
+ return {
655
+ scrollState,
656
+ onScroll,
657
+ onScrollBeginDrag,
658
+ onScrollEndDrag,
659
+ onMomentumScrollBegin,
660
+ onMomentumScrollEnd,
661
+ onContentSizeChange
662
+ };
663
+ }
664
+ /**
665
+ * Hook to calculate adaptive draw distance based on scroll velocity.
666
+ * Increases buffer for fast scrolling to reduce blank areas.
667
+ */
668
+ function useAdaptiveDrawDistance(baseDistance, scrollVelocity) {
669
+ return (0, react.useMemo)(() => {
670
+ const absVelocity = Math.abs(scrollVelocity);
671
+ if (absVelocity < SCROLL_VELOCITY_THRESHOLD) return baseDistance;
672
+ const velocityMultiplier = Math.min(3, 1 + absVelocity / SCROLL_VELOCITY_THRESHOLD);
673
+ return Math.round(baseDistance * velocityMultiplier);
674
+ }, [baseDistance, scrollVelocity]);
675
+ }
676
+
677
+ //#endregion
678
+ //#region src/core/CellRecycler.ts
679
+ /**
680
+ * CellRecycler manages pools of recycled cell instances per item type.
681
+ * This is the core of FlashList-style performance - reusing view components
682
+ * instead of destroying and recreating them.
683
+ */
684
+ var CellRecyclerImpl = class {
685
+ pools = new Map();
686
+ inUse = new Map();
687
+ cellCounter = 0;
688
+ constructor(defaultMaxPoolSize = DEFAULT_MAX_POOL_SIZE) {
689
+ this.defaultMaxPoolSize = defaultMaxPoolSize;
690
+ }
691
+ /**
692
+ * Acquire a cell from the pool for the given type.
693
+ * Returns null if no recycled cells are available (caller should create new).
694
+ */
695
+ acquireCell(type, flatIndex) {
696
+ const pool = this.pools.get(type);
697
+ if (!pool || pool.cells.length === 0) return null;
698
+ const cell = pool.cells.pop();
699
+ cell.flatIndex = flatIndex;
700
+ let inUseSet = this.inUse.get(type);
701
+ if (!inUseSet) {
702
+ inUseSet = new Set();
703
+ this.inUse.set(type, inUseSet);
704
+ }
705
+ inUseSet.add(cell.key);
706
+ return cell;
707
+ }
708
+ /**
709
+ * Release a cell back to its pool for recycling.
710
+ */
711
+ releaseCell(cell) {
712
+ const { itemType: type, key } = cell;
713
+ const inUseSet = this.inUse.get(type);
714
+ if (inUseSet) inUseSet.delete(key);
715
+ let pool = this.pools.get(type);
716
+ if (!pool) {
717
+ pool = {
718
+ type,
719
+ cells: [],
720
+ maxSize: this.defaultMaxPoolSize
721
+ };
722
+ this.pools.set(type, pool);
723
+ }
724
+ if (pool.cells.length < pool.maxSize) pool.cells.push(cell);
725
+ }
726
+ /**
727
+ * Clear all recycled cells from all pools.
728
+ * Useful when data changes significantly.
729
+ */
730
+ clearPools() {
731
+ this.pools.clear();
732
+ this.inUse.clear();
733
+ }
734
+ /**
735
+ * Set the maximum pool size for a specific item type.
736
+ */
737
+ setMaxPoolSize(type, size) {
738
+ let pool = this.pools.get(type);
739
+ if (!pool) {
740
+ pool = {
741
+ type,
742
+ cells: [],
743
+ maxSize: size
744
+ };
745
+ this.pools.set(type, pool);
746
+ } else {
747
+ pool.maxSize = size;
748
+ while (pool.cells.length > size) pool.cells.pop();
749
+ }
750
+ }
751
+ /**
752
+ * Get statistics about pool usage for debugging.
753
+ */
754
+ getPoolStats() {
755
+ const stats = new Map();
756
+ const allTypes = new Set([...this.pools.keys(), ...this.inUse.keys()]);
757
+ for (const type of allTypes) {
758
+ const pool = this.pools.get(type);
759
+ const inUseSet = this.inUse.get(type);
760
+ stats.set(type, {
761
+ available: pool?.cells.length ?? 0,
762
+ inUse: inUseSet?.size ?? 0
763
+ });
764
+ }
765
+ return stats;
766
+ }
767
+ /**
768
+ * Generate a unique key for a new cell.
769
+ */
770
+ generateCellKey(type) {
771
+ return `${type}-${++this.cellCounter}`;
772
+ }
773
+ /**
774
+ * Create a new cell (when pool is empty).
775
+ */
776
+ createCell(type, flatIndex) {
777
+ const key = this.generateCellKey(type);
778
+ const cell = {
779
+ key,
780
+ itemType: type,
781
+ flatIndex
782
+ };
783
+ let inUseSet = this.inUse.get(type);
784
+ if (!inUseSet) {
785
+ inUseSet = new Set();
786
+ this.inUse.set(type, inUseSet);
787
+ }
788
+ inUseSet.add(key);
789
+ return cell;
790
+ }
791
+ /**
792
+ * Get or create a cell - the main method used during rendering.
793
+ * First tries to acquire from pool, then creates new if needed.
794
+ */
795
+ getCell(type, flatIndex) {
796
+ const recycled = this.acquireCell(type, flatIndex);
797
+ if (recycled) return recycled;
798
+ return this.createCell(type, flatIndex);
799
+ }
800
+ };
801
+ /**
802
+ * Factory function to create a CellRecycler instance.
803
+ */
804
+ function createCellRecycler(defaultMaxPoolSize = DEFAULT_MAX_POOL_SIZE) {
805
+ return new CellRecyclerImpl(defaultMaxPoolSize);
806
+ }
807
+
808
+ //#endregion
809
+ //#region src/hooks/useRecycler.ts
810
+ /**
811
+ * Hook for managing cell recycling state.
812
+ * Provides methods to acquire and release cells based on visibility.
813
+ */
814
+ function useRecycler(options) {
815
+ const { flattenedData, getItemType, maxPoolSize } = options;
816
+ const recycler = (0, react.useRef)(null);
817
+ if (!recycler.current) recycler.current = createCellRecycler(maxPoolSize);
818
+ const activeCells = (0, react.useRef)(new Map());
819
+ const visibleRange = (0, react.useRef)({
820
+ start: 0,
821
+ end: -1
822
+ });
823
+ /**
824
+ * Get the item type for a flat index.
825
+ */
826
+ const getTypeForIndex = (0, react.useCallback)((flatIndex) => {
827
+ const item = flattenedData[flatIndex];
828
+ if (!item) return DEFAULT_ITEM_TYPE;
829
+ if (item.type === "section-header") return SECTION_HEADER_TYPE;
830
+ if (item.type === "section-footer") return SECTION_FOOTER_TYPE;
831
+ if (getItemType && item.item !== null) return getItemType(item.item, item.itemIndex);
832
+ return DEFAULT_ITEM_TYPE;
833
+ }, [flattenedData, getItemType]);
834
+ /**
835
+ * Get or create a cell for a flat index.
836
+ */
837
+ const getCell = (0, react.useCallback)((flatIndex) => {
838
+ const existing = activeCells.current.get(flatIndex);
839
+ if (existing) return existing;
840
+ const type = getTypeForIndex(flatIndex);
841
+ const cell = recycler.current.getCell(type, flatIndex);
842
+ activeCells.current.set(flatIndex, cell);
843
+ return cell;
844
+ }, [getTypeForIndex]);
845
+ /**
846
+ * Release a cell back to the pool.
847
+ */
848
+ const releaseCell = (0, react.useCallback)((cell) => {
849
+ activeCells.current.delete(cell.flatIndex);
850
+ recycler.current.releaseCell(cell);
851
+ }, []);
852
+ /**
853
+ * Update the visible range and recycle cells outside it.
854
+ */
855
+ const updateVisibleRange = (0, react.useCallback)((startIndex, endIndex) => {
856
+ const prevStart = visibleRange.current.start;
857
+ const prevEnd = visibleRange.current.end;
858
+ visibleRange.current = {
859
+ start: startIndex,
860
+ end: endIndex
861
+ };
862
+ for (const [flatIndex, cell] of activeCells.current) if (flatIndex < startIndex || flatIndex > endIndex) releaseCell(cell);
863
+ }, [releaseCell]);
864
+ /**
865
+ * Clear all pools (e.g., on data change).
866
+ */
867
+ const clearPools = (0, react.useCallback)(() => {
868
+ activeCells.current.clear();
869
+ recycler.current.clearPools();
870
+ }, []);
871
+ /**
872
+ * Get pool statistics for debugging.
873
+ */
874
+ const getPoolStats = (0, react.useCallback)(() => {
875
+ return recycler.current.getPoolStats();
876
+ }, []);
877
+ const dataLength = flattenedData.length;
878
+ (0, react.useEffect)(() => {
879
+ if (activeCells.current.size > dataLength * 2) clearPools();
880
+ }, [dataLength, clearPools]);
881
+ return {
882
+ getCell,
883
+ releaseCell,
884
+ updateVisibleRange,
885
+ clearPools,
886
+ getPoolStats
887
+ };
888
+ }
889
+ /**
890
+ * Hook to determine which item type to use for rendering.
891
+ * Returns a stable function that maps flat indices to type strings.
892
+ */
893
+ function useItemTypeResolver(flattenedData, getItemType) {
894
+ return (0, react.useCallback)((flatIndex) => {
895
+ const item = flattenedData[flatIndex];
896
+ if (!item) return DEFAULT_ITEM_TYPE;
897
+ if (item.type === "section-header") return SECTION_HEADER_TYPE;
898
+ if (item.type === "section-footer") return SECTION_FOOTER_TYPE;
899
+ if (getItemType && item.item !== null) return getItemType(item.item, item.itemIndex);
900
+ return DEFAULT_ITEM_TYPE;
901
+ }, [flattenedData, getItemType]);
902
+ }
903
+
904
+ //#endregion
905
+ //#region src/hooks/useStickyHeader.ts
906
+ /**
907
+ * Hook for computing sticky header positioning.
908
+ * Handles the "pushing" effect when the next section header approaches.
909
+ */
910
+ function useStickyHeader(options) {
911
+ const { sectionLayouts, scrollOffset, viewportHeight, horizontal = false, enabled = true } = options;
912
+ return (0, react.useMemo)(() => {
913
+ if (!enabled || sectionLayouts.length === 0) return {
914
+ sectionKey: null,
915
+ sectionIndex: -1,
916
+ translateY: 0,
917
+ isSticky: false,
918
+ headerLayout: null
919
+ };
920
+ let currentSection = null;
921
+ let nextSection = null;
922
+ for (let i = 0; i < sectionLayouts.length; i++) {
923
+ const section = sectionLayouts[i];
924
+ const headerStart = horizontal ? section.headerLayout.x : section.headerLayout.y;
925
+ if (headerStart <= scrollOffset) {
926
+ currentSection = section;
927
+ nextSection = sectionLayouts[i + 1] ?? null;
928
+ } else break;
929
+ }
930
+ if (!currentSection) return {
931
+ sectionKey: null,
932
+ sectionIndex: -1,
933
+ translateY: 0,
934
+ isSticky: false,
935
+ headerLayout: null
936
+ };
937
+ const headerSize = horizontal ? currentSection.headerLayout.width : currentSection.headerLayout.height;
938
+ let translateY = 0;
939
+ if (nextSection) {
940
+ const nextHeaderStart = horizontal ? nextSection.headerLayout.x : nextSection.headerLayout.y;
941
+ const pushPoint = nextHeaderStart - headerSize;
942
+ if (scrollOffset > pushPoint) translateY = pushPoint - scrollOffset;
943
+ }
944
+ return {
945
+ sectionKey: currentSection.sectionKey,
946
+ sectionIndex: currentSection.sectionIndex,
947
+ translateY,
948
+ isSticky: true,
949
+ headerLayout: currentSection.headerLayout
950
+ };
951
+ }, [
952
+ sectionLayouts,
953
+ scrollOffset,
954
+ viewportHeight,
955
+ horizontal,
956
+ enabled
957
+ ]);
958
+ }
959
+ /**
960
+ * Hook for tracking multiple sticky headers (e.g., for multi-level sections).
961
+ */
962
+ function useMultipleStickyHeaders(sectionLayouts, scrollOffset, levels = 1) {
963
+ return (0, react.useMemo)(() => {
964
+ const states = [];
965
+ if (sectionLayouts.length === 0) return states;
966
+ let currentSection = null;
967
+ let nextSection = null;
968
+ for (let i = 0; i < sectionLayouts.length; i++) {
969
+ const section = sectionLayouts[i];
970
+ const headerStart = section.headerLayout.y;
971
+ if (headerStart <= scrollOffset) {
972
+ currentSection = section;
973
+ nextSection = sectionLayouts[i + 1] ?? null;
974
+ } else break;
975
+ }
976
+ if (currentSection) {
977
+ const headerSize = currentSection.headerLayout.height;
978
+ let translateY = 0;
979
+ if (nextSection) {
980
+ const nextHeaderStart = nextSection.headerLayout.y;
981
+ const pushPoint = nextHeaderStart - headerSize;
982
+ if (scrollOffset > pushPoint) translateY = pushPoint - scrollOffset;
983
+ }
984
+ states.push({
985
+ sectionKey: currentSection.sectionKey,
986
+ sectionIndex: currentSection.sectionIndex,
987
+ translateY,
988
+ isSticky: true,
989
+ headerLayout: currentSection.headerLayout
990
+ });
991
+ }
992
+ return states;
993
+ }, [
994
+ sectionLayouts,
995
+ scrollOffset,
996
+ levels
997
+ ]);
998
+ }
999
+ /**
1000
+ * Hook for determining sticky header opacity during transitions.
1001
+ */
1002
+ function useStickyHeaderOpacity(stickyState, fadeDistance = 20) {
1003
+ return (0, react.useMemo)(() => {
1004
+ if (!stickyState.isSticky) return 0;
1005
+ if (stickyState.translateY >= 0) return 1;
1006
+ const fadeProgress = Math.abs(stickyState.translateY) / fadeDistance;
1007
+ return Math.max(0, 1 - fadeProgress);
1008
+ }, [
1009
+ stickyState.isSticky,
1010
+ stickyState.translateY,
1011
+ fadeDistance
1012
+ ]);
1013
+ }
1014
+
1015
+ //#endregion
1016
+ //#region src/core/ViewabilityTracker.ts
1017
+ var ViewabilityTrackerImpl = class {
1018
+ flattenedData = [];
1019
+ scrollOffset = 0;
1020
+ viewportSize = 0;
1021
+ trackedItems = new Map();
1022
+ currentlyViewable = new Set();
1023
+ callbacks = new Set();
1024
+ hasInteracted = false;
1025
+ pendingUpdate = null;
1026
+ constructor(layoutPositioner, flattenedData, config = {}, horizontal = false) {
1027
+ this.layoutPositioner = layoutPositioner;
1028
+ this.flattenedData = flattenedData;
1029
+ this.horizontal = horizontal;
1030
+ this.config = {
1031
+ minimumViewTime: config.minimumViewTime ?? DEFAULT_VIEWABILITY_CONFIG.minimumViewTime,
1032
+ viewAreaCoveragePercentThreshold: config.viewAreaCoveragePercentThreshold ?? DEFAULT_VIEWABILITY_CONFIG.viewAreaCoveragePercentThreshold,
1033
+ itemVisiblePercentThreshold: config.itemVisiblePercentThreshold ?? DEFAULT_VIEWABILITY_CONFIG.itemVisiblePercentThreshold,
1034
+ waitForInteraction: config.waitForInteraction ?? DEFAULT_VIEWABILITY_CONFIG.waitForInteraction
1035
+ };
1036
+ }
1037
+ /**
1038
+ * Update flattened data when it changes.
1039
+ */
1040
+ setData(flattenedData) {
1041
+ this.flattenedData = flattenedData;
1042
+ this.trackedItems.clear();
1043
+ this.currentlyViewable.clear();
1044
+ this.scheduleUpdate();
1045
+ }
1046
+ /**
1047
+ * Update the current scroll offset.
1048
+ */
1049
+ updateScrollOffset(offset) {
1050
+ this.scrollOffset = offset;
1051
+ this.scheduleUpdate();
1052
+ }
1053
+ /**
1054
+ * Update the viewport size.
1055
+ */
1056
+ setViewportSize(size) {
1057
+ if (this.viewportSize !== size) {
1058
+ this.viewportSize = size;
1059
+ this.scheduleUpdate();
1060
+ }
1061
+ }
1062
+ /**
1063
+ * Record that user has interacted with the list.
1064
+ */
1065
+ recordInteraction() {
1066
+ if (!this.hasInteracted) {
1067
+ this.hasInteracted = true;
1068
+ this.scheduleUpdate();
1069
+ }
1070
+ }
1071
+ /**
1072
+ * Schedule a viewability update (debounced).
1073
+ */
1074
+ scheduleUpdate() {
1075
+ if (this.pendingUpdate) return;
1076
+ this.pendingUpdate = setTimeout(() => {
1077
+ this.pendingUpdate = null;
1078
+ this.computeViewability();
1079
+ }, 0);
1080
+ }
1081
+ /**
1082
+ * Compute which items are viewable based on current scroll position.
1083
+ */
1084
+ computeViewability() {
1085
+ if (this.config.waitForInteraction && !this.hasInteracted) return;
1086
+ const now = Date.now();
1087
+ const newViewable = new Set();
1088
+ const changed = [];
1089
+ const { startIndex, endIndex } = this.layoutPositioner.getVisibleRange(this.scrollOffset, this.viewportSize, 0);
1090
+ for (let i = startIndex; i <= endIndex; i++) {
1091
+ const layout = this.layoutPositioner.getLayoutForIndex(i);
1092
+ const isViewable = this.isItemViewable(i, layout);
1093
+ if (isViewable) {
1094
+ newViewable.add(i);
1095
+ let tracked = this.trackedItems.get(i);
1096
+ if (!tracked) {
1097
+ tracked = {
1098
+ flatIndex: i,
1099
+ isViewable: false,
1100
+ lastVisibleTime: now,
1101
+ becameVisibleAt: now
1102
+ };
1103
+ this.trackedItems.set(i, tracked);
1104
+ }
1105
+ if (!tracked.isViewable && tracked.becameVisibleAt !== null) {
1106
+ const visibleDuration = now - tracked.becameVisibleAt;
1107
+ if (visibleDuration >= this.config.minimumViewTime) {
1108
+ tracked.isViewable = true;
1109
+ changed.push(this.createViewToken(i, true));
1110
+ }
1111
+ }
1112
+ tracked.lastVisibleTime = now;
1113
+ }
1114
+ }
1115
+ for (const flatIndex of this.currentlyViewable) if (!newViewable.has(flatIndex)) {
1116
+ const tracked = this.trackedItems.get(flatIndex);
1117
+ if (tracked && tracked.isViewable) {
1118
+ tracked.isViewable = false;
1119
+ tracked.becameVisibleAt = null;
1120
+ changed.push(this.createViewToken(flatIndex, false));
1121
+ }
1122
+ }
1123
+ this.currentlyViewable = newViewable;
1124
+ if (changed.length > 0) {
1125
+ const viewableItems = Array.from(this.currentlyViewable).filter((i) => this.trackedItems.get(i)?.isViewable).map((i) => this.createViewToken(i, true));
1126
+ for (const callback of this.callbacks) callback({
1127
+ viewableItems,
1128
+ changed
1129
+ });
1130
+ }
1131
+ }
1132
+ /**
1133
+ * Check if an item meets viewability thresholds.
1134
+ */
1135
+ isItemViewable(flatIndex, layout) {
1136
+ const itemStart = this.horizontal ? layout.x : layout.y;
1137
+ const itemSize = this.horizontal ? layout.width : layout.height;
1138
+ const itemEnd = itemStart + itemSize;
1139
+ const viewportStart = this.scrollOffset;
1140
+ const viewportEnd = this.scrollOffset + this.viewportSize;
1141
+ const visibleStart = Math.max(itemStart, viewportStart);
1142
+ const visibleEnd = Math.min(itemEnd, viewportEnd);
1143
+ const visibleSize = Math.max(0, visibleEnd - visibleStart);
1144
+ if (this.config.itemVisiblePercentThreshold > 0) {
1145
+ const visiblePercent = itemSize > 0 ? visibleSize / itemSize * 100 : 0;
1146
+ if (visiblePercent < this.config.itemVisiblePercentThreshold) return false;
1147
+ }
1148
+ if (this.config.viewAreaCoveragePercentThreshold > 0) {
1149
+ const coveragePercent = this.viewportSize > 0 ? visibleSize / this.viewportSize * 100 : 0;
1150
+ if (coveragePercent < this.config.viewAreaCoveragePercentThreshold) return false;
1151
+ }
1152
+ return visibleSize > 0;
1153
+ }
1154
+ /**
1155
+ * Create a ViewToken for an item.
1156
+ */
1157
+ createViewToken(flatIndex, isViewable) {
1158
+ const item = this.flattenedData[flatIndex];
1159
+ return {
1160
+ item: item?.item ?? null,
1161
+ key: item?.key ?? `item-${flatIndex}`,
1162
+ index: flatIndex,
1163
+ isViewable,
1164
+ section: item?.section
1165
+ };
1166
+ }
1167
+ /**
1168
+ * Get all currently visible indices.
1169
+ */
1170
+ getVisibleIndices() {
1171
+ return Array.from(this.currentlyViewable);
1172
+ }
1173
+ /**
1174
+ * Check if a specific index is visible.
1175
+ */
1176
+ isIndexVisible(index) {
1177
+ return this.currentlyViewable.has(index);
1178
+ }
1179
+ /**
1180
+ * Get the first visible index.
1181
+ */
1182
+ getFirstVisibleIndex() {
1183
+ if (this.currentlyViewable.size === 0) return -1;
1184
+ return Math.min(...this.currentlyViewable);
1185
+ }
1186
+ /**
1187
+ * Get the last visible index.
1188
+ */
1189
+ getLastVisibleIndex() {
1190
+ if (this.currentlyViewable.size === 0) return -1;
1191
+ return Math.max(...this.currentlyViewable);
1192
+ }
1193
+ /**
1194
+ * Register a callback for viewability changes.
1195
+ */
1196
+ onViewableItemsChanged(callback) {
1197
+ this.callbacks.add(callback);
1198
+ return () => {
1199
+ this.callbacks.delete(callback);
1200
+ };
1201
+ }
1202
+ /**
1203
+ * Clean up resources.
1204
+ */
1205
+ dispose() {
1206
+ if (this.pendingUpdate) {
1207
+ clearTimeout(this.pendingUpdate);
1208
+ this.pendingUpdate = null;
1209
+ }
1210
+ this.callbacks.clear();
1211
+ this.trackedItems.clear();
1212
+ this.currentlyViewable.clear();
1213
+ }
1214
+ };
1215
+ /**
1216
+ * Factory function to create a ViewabilityTracker.
1217
+ */
1218
+ function createViewabilityTracker(layoutPositioner, flattenedData, config, horizontal) {
1219
+ return new ViewabilityTrackerImpl(layoutPositioner, flattenedData, config, horizontal);
1220
+ }
1221
+
1222
+ //#endregion
1223
+ //#region src/hooks/useViewability.ts
1224
+ /**
1225
+ * Hook for tracking item viewability and triggering callbacks.
1226
+ */
1227
+ function useViewability(options) {
1228
+ const { flattenedData, layoutPositioner, scrollOffset, viewportSize, horizontal = false, viewabilityConfig = DEFAULT_VIEWABILITY_CONFIG, onViewableItemsChanged } = options;
1229
+ const trackerRef = (0, react.useRef)(null);
1230
+ if (!trackerRef.current) trackerRef.current = createViewabilityTracker(layoutPositioner, flattenedData, viewabilityConfig, horizontal);
1231
+ const tracker = trackerRef.current;
1232
+ (0, react.useEffect)(() => {
1233
+ tracker.setData(flattenedData);
1234
+ }, [tracker, flattenedData]);
1235
+ (0, react.useEffect)(() => {
1236
+ tracker.setViewportSize(viewportSize);
1237
+ }, [tracker, viewportSize]);
1238
+ (0, react.useEffect)(() => {
1239
+ tracker.updateScrollOffset(scrollOffset);
1240
+ }, [tracker, scrollOffset]);
1241
+ (0, react.useEffect)(() => {
1242
+ if (!onViewableItemsChanged) return;
1243
+ const unsubscribe = tracker.onViewableItemsChanged(onViewableItemsChanged);
1244
+ return unsubscribe;
1245
+ }, [tracker, onViewableItemsChanged]);
1246
+ (0, react.useEffect)(() => {
1247
+ return () => {
1248
+ tracker.dispose();
1249
+ };
1250
+ }, [tracker]);
1251
+ const recordInteraction = (0, react.useCallback)(() => {
1252
+ tracker.recordInteraction();
1253
+ }, [tracker]);
1254
+ const getVisibleItems = (0, react.useCallback)(() => {
1255
+ return tracker.getVisibleIndices().map((flatIndex) => {
1256
+ const item = flattenedData[flatIndex];
1257
+ return {
1258
+ item: item?.item ?? null,
1259
+ key: item?.key ?? `item-${flatIndex}`,
1260
+ index: flatIndex,
1261
+ isViewable: true,
1262
+ section: item?.section
1263
+ };
1264
+ });
1265
+ }, [tracker, flattenedData]);
1266
+ const visibleState = (0, react.useMemo)(() => {
1267
+ const indices = tracker.getVisibleIndices();
1268
+ return {
1269
+ visibleIndices: indices,
1270
+ firstVisibleIndex: tracker.getFirstVisibleIndex(),
1271
+ lastVisibleIndex: tracker.getLastVisibleIndex()
1272
+ };
1273
+ }, [
1274
+ tracker,
1275
+ scrollOffset,
1276
+ viewportSize
1277
+ ]);
1278
+ return {
1279
+ ...visibleState,
1280
+ getVisibleItems,
1281
+ recordInteraction
1282
+ };
1283
+ }
1284
+ /**
1285
+ * Hook for handling multiple viewability configs with different callbacks.
1286
+ */
1287
+ function useMultipleViewabilityConfigs(flattenedData, layoutPositioner, scrollOffset, viewportSize, configs, horizontal = false) {
1288
+ const trackersRef = (0, react.useRef)([]);
1289
+ (0, react.useEffect)(() => {
1290
+ trackersRef.current = configs.map((config) => createViewabilityTracker(layoutPositioner, flattenedData, config.viewabilityConfig, horizontal));
1291
+ const unsubscribes = trackersRef.current.map((tracker, index) => tracker.onViewableItemsChanged(configs[index].onViewableItemsChanged));
1292
+ return () => {
1293
+ unsubscribes.forEach((unsub) => unsub());
1294
+ trackersRef.current.forEach((tracker) => tracker.dispose());
1295
+ trackersRef.current = [];
1296
+ };
1297
+ }, [
1298
+ configs,
1299
+ flattenedData,
1300
+ layoutPositioner,
1301
+ horizontal
1302
+ ]);
1303
+ (0, react.useEffect)(() => {
1304
+ trackersRef.current.forEach((tracker) => tracker.setData(flattenedData));
1305
+ }, [flattenedData]);
1306
+ (0, react.useEffect)(() => {
1307
+ trackersRef.current.forEach((tracker) => tracker.setViewportSize(viewportSize));
1308
+ }, [viewportSize]);
1309
+ (0, react.useEffect)(() => {
1310
+ trackersRef.current.forEach((tracker) => tracker.updateScrollOffset(scrollOffset));
1311
+ }, [scrollOffset]);
1312
+ }
1313
+
1314
+ //#endregion
1315
+ //#region src/state/SectionFlowContext.tsx
1316
+ const SectionFlowContext = (0, react.createContext)(void 0);
1317
+ /**
1318
+ * Hook to access SectionFlow context.
1319
+ * Must be used within a SectionFlowProvider.
1320
+ */
1321
+ function useSectionFlowContext() {
1322
+ const context = (0, react.useContext)(SectionFlowContext);
1323
+ if (!context) throw new Error("useSectionFlowContext must be used within a SectionFlowProvider");
1324
+ return context;
1325
+ }
1326
+ /**
1327
+ * Provider component for SectionFlow context.
1328
+ */
1329
+ function SectionFlowProvider({ children, sections, flattenedData, layoutManager, layoutCache, horizontal, collapsedSections, scrollOffset, viewportWidth, viewportHeight, stickySectionHeadersEnabled, estimatedItemSize, drawDistance, debug, renderItem, renderSectionHeader, renderSectionFooter, onCellMeasured, onSectionToggle, getItemType }) {
1330
+ const value = (0, react.useMemo)(() => ({
1331
+ sections,
1332
+ flattenedData,
1333
+ layoutManager,
1334
+ layoutCache,
1335
+ horizontal,
1336
+ collapsedSections,
1337
+ scrollOffset,
1338
+ viewportWidth,
1339
+ viewportHeight,
1340
+ stickySectionHeadersEnabled,
1341
+ estimatedItemSize,
1342
+ drawDistance,
1343
+ debug,
1344
+ renderItem,
1345
+ renderSectionHeader,
1346
+ renderSectionFooter,
1347
+ onCellMeasured,
1348
+ onSectionToggle,
1349
+ getItemType
1350
+ }), [
1351
+ sections,
1352
+ flattenedData,
1353
+ layoutManager,
1354
+ layoutCache,
1355
+ horizontal,
1356
+ collapsedSections,
1357
+ scrollOffset,
1358
+ viewportWidth,
1359
+ viewportHeight,
1360
+ stickySectionHeadersEnabled,
1361
+ estimatedItemSize,
1362
+ drawDistance,
1363
+ debug,
1364
+ renderItem,
1365
+ renderSectionHeader,
1366
+ renderSectionFooter,
1367
+ onCellMeasured,
1368
+ onSectionToggle,
1369
+ getItemType
1370
+ ]);
1371
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionFlowContext.Provider, {
1372
+ value,
1373
+ children
1374
+ });
1375
+ }
1376
+
1377
+ //#endregion
1378
+ //#region src/components/RecyclerCell.tsx
1379
+ /**
1380
+ * RecyclerCell wraps each item with absolute positioning.
1381
+ * This is the fundamental unit of the recycling system.
1382
+ *
1383
+ * Key responsibilities:
1384
+ * 1. Position the item at the correct x/y coordinates
1385
+ * 2. Report layout measurements back to the system
1386
+ * 3. Maintain stable identity for recycling (via key)
1387
+ */
1388
+ function RecyclerCellComponent({ cell, layout, children, onLayout, debug = false }) {
1389
+ const handleLayout = (0, react.useCallback)((event) => {
1390
+ const { x, y, width, height } = event.nativeEvent.layout;
1391
+ onLayout(cell.key, cell.flatIndex, {
1392
+ x,
1393
+ y,
1394
+ width,
1395
+ height
1396
+ });
1397
+ }, [
1398
+ cell.key,
1399
+ cell.flatIndex,
1400
+ onLayout
1401
+ ]);
1402
+ const positionStyle = {
1403
+ position: "absolute",
1404
+ left: layout.x,
1405
+ top: layout.y,
1406
+ width: layout.width
1407
+ };
1408
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
1409
+ style: [positionStyle, debug && styles$4.debug],
1410
+ onLayout: handleLayout,
1411
+ children
1412
+ });
1413
+ }
1414
+ const styles$4 = react_native.StyleSheet.create({ debug: {
1415
+ borderWidth: 1,
1416
+ borderColor: "rgba(255, 0, 0, 0.3)"
1417
+ } });
1418
+ /**
1419
+ * Memoized cell component to prevent unnecessary re-renders.
1420
+ * Only re-renders when:
1421
+ * - Cell key changes (different item)
1422
+ * - Layout position changes
1423
+ * - Children change (new render)
1424
+ */
1425
+ const RecyclerCell = (0, react.memo)(RecyclerCellComponent, (prevProps, nextProps) => {
1426
+ return prevProps.cell.key === nextProps.cell.key && prevProps.cell.flatIndex === nextProps.cell.flatIndex && prevProps.layout.x === nextProps.layout.x && prevProps.layout.y === nextProps.layout.y && prevProps.layout.width === nextProps.layout.width && prevProps.layout.height === nextProps.layout.height && prevProps.children === nextProps.children && prevProps.debug === nextProps.debug;
1427
+ });
1428
+ RecyclerCell.displayName = "RecyclerCell";
1429
+
1430
+ //#endregion
1431
+ //#region src/components/RecyclerContainer.tsx
1432
+ /**
1433
+ * RecyclerContainer manages the absolute-positioned content area.
1434
+ * It renders only the visible items within the current scroll window.
1435
+ *
1436
+ * Key responsibilities:
1437
+ * 1. Set content size for scroll container
1438
+ * 2. Render visible items at correct positions
1439
+ * 3. Coordinate cell recycling
1440
+ */
1441
+ function RecyclerContainerComponent({ flattenedData, visibleRange, getLayoutForIndex, getCell, contentSize, renderItem, renderSectionHeader, renderSectionFooter, onCellLayout, horizontal = false, debug = false }) {
1442
+ const { startIndex, endIndex } = visibleRange;
1443
+ const cells = (0, react.useMemo)(() => {
1444
+ const result = [];
1445
+ for (let i = startIndex; i <= endIndex; i++) {
1446
+ const item = flattenedData[i];
1447
+ if (!item) continue;
1448
+ const cell = getCell(i);
1449
+ const layout = getLayoutForIndex(i);
1450
+ let content = null;
1451
+ switch (item.type) {
1452
+ case "section-header":
1453
+ if (renderSectionHeader) content = renderSectionHeader({
1454
+ section: item.section,
1455
+ sectionIndex: item.sectionIndex
1456
+ });
1457
+ break;
1458
+ case "section-footer":
1459
+ if (renderSectionFooter) content = renderSectionFooter({
1460
+ section: item.section,
1461
+ sectionIndex: item.sectionIndex
1462
+ });
1463
+ break;
1464
+ case "item":
1465
+ if (item.item !== null) content = renderItem({
1466
+ item: item.item,
1467
+ index: item.itemIndex,
1468
+ section: item.section,
1469
+ sectionIndex: item.sectionIndex
1470
+ });
1471
+ break;
1472
+ }
1473
+ if (content) result.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecyclerCell, {
1474
+ cell,
1475
+ layout,
1476
+ onLayout: onCellLayout,
1477
+ debug,
1478
+ children: content
1479
+ }, cell.key));
1480
+ }
1481
+ return result;
1482
+ }, [
1483
+ startIndex,
1484
+ endIndex,
1485
+ flattenedData,
1486
+ getCell,
1487
+ getLayoutForIndex,
1488
+ renderItem,
1489
+ renderSectionHeader,
1490
+ renderSectionFooter,
1491
+ onCellLayout,
1492
+ debug
1493
+ ]);
1494
+ const containerStyle = (0, react.useMemo)(() => ({
1495
+ width: contentSize.width || "100%",
1496
+ height: contentSize.height,
1497
+ position: "relative"
1498
+ }), [contentSize.width, contentSize.height]);
1499
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
1500
+ style: [containerStyle, debug && styles$3.debug],
1501
+ children: cells
1502
+ });
1503
+ }
1504
+ const styles$3 = react_native.StyleSheet.create({ debug: { backgroundColor: "rgba(0, 255, 0, 0.05)" } });
1505
+ const RecyclerContainer = (0, react.memo)(RecyclerContainerComponent);
1506
+
1507
+ //#endregion
1508
+ //#region src/components/StickyHeaderContainer.tsx
1509
+ /**
1510
+ * Container for the sticky section header.
1511
+ * Positioned at the top (or left for horizontal) of the viewport.
1512
+ * Handles the "push" effect when the next section approaches.
1513
+ */
1514
+ function StickyHeaderContainerComponent({ stickySection, translateY, sections, renderSectionHeader, horizontal = false, style }) {
1515
+ if (!stickySection) return null;
1516
+ const section = sections.find((s) => s.key === stickySection.sectionKey);
1517
+ if (!section) return null;
1518
+ const headerContent = renderSectionHeader({
1519
+ section,
1520
+ sectionIndex: stickySection.sectionIndex
1521
+ });
1522
+ if (!headerContent) return null;
1523
+ const containerStyle = (0, react.useMemo)(() => ({
1524
+ position: "absolute",
1525
+ top: horizontal ? void 0 : 0,
1526
+ left: horizontal ? 0 : 0,
1527
+ right: horizontal ? void 0 : 0,
1528
+ transform: horizontal ? [{ translateX: translateY }] : [{ translateY }],
1529
+ zIndex: 1e3,
1530
+ elevation: 4
1531
+ }), [horizontal, translateY]);
1532
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
1533
+ style: [
1534
+ containerStyle,
1535
+ styles$2.shadow,
1536
+ style
1537
+ ],
1538
+ pointerEvents: "box-none",
1539
+ children: headerContent
1540
+ });
1541
+ }
1542
+ const styles$2 = react_native.StyleSheet.create({ shadow: {
1543
+ shadowColor: "#000",
1544
+ shadowOffset: {
1545
+ width: 0,
1546
+ height: 2
1547
+ },
1548
+ shadowOpacity: .1,
1549
+ shadowRadius: 4
1550
+ } });
1551
+ const StickyHeaderContainer = (0, react.memo)(StickyHeaderContainerComponent);
1552
+ function AnimatedStickyHeaderContainerComponent({ stickySection, animatedTranslateY, sections, renderSectionHeader, horizontal = false, style }) {
1553
+ if (!stickySection) return null;
1554
+ const section = sections.find((s) => s.key === stickySection.sectionKey);
1555
+ if (!section) return null;
1556
+ const headerContent = renderSectionHeader({
1557
+ section,
1558
+ sectionIndex: stickySection.sectionIndex
1559
+ });
1560
+ if (!headerContent) return null;
1561
+ const animatedStyle = (0, react.useMemo)(() => ({
1562
+ position: "absolute",
1563
+ top: horizontal ? void 0 : 0,
1564
+ left: 0,
1565
+ right: horizontal ? void 0 : 0,
1566
+ transform: horizontal ? [{ translateX: animatedTranslateY }] : [{ translateY: animatedTranslateY }],
1567
+ zIndex: 1e3,
1568
+ elevation: 4
1569
+ }), [horizontal, animatedTranslateY]);
1570
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Animated.View, {
1571
+ style: [
1572
+ animatedStyle,
1573
+ styles$2.shadow,
1574
+ style
1575
+ ],
1576
+ pointerEvents: "box-none",
1577
+ children: headerContent
1578
+ });
1579
+ }
1580
+ const AnimatedStickyHeaderContainer = (0, react.memo)(AnimatedStickyHeaderContainerComponent);
1581
+
1582
+ //#endregion
1583
+ //#region src/components/SectionFlow.tsx
1584
+ /**
1585
+ * SectionFlow - High-performance section list for React Native
1586
+ *
1587
+ * A drop-in replacement for SectionList with FlashList-style cell recycling.
1588
+ * Provides smooth 60fps scrolling through:
1589
+ * - Cell recycling (reuses views instead of creating new ones)
1590
+ * - Synchronous measurements (New Architecture)
1591
+ * - Type-based recycle pools
1592
+ * - Absolute positioning with computed layouts
1593
+ */
1594
+ function SectionFlowInner(props, ref) {
1595
+ const { sections, renderItem, renderSectionHeader, renderSectionFooter, keyExtractor, getItemType, estimatedItemSize = DEFAULT_ESTIMATED_ITEM_SIZE, estimatedSectionHeaderSize = DEFAULT_ESTIMATED_HEADER_SIZE, estimatedSectionFooterSize = 0, horizontal = false, stickySectionHeadersEnabled = true, stickyHeaderStyle, collapsible = false, defaultCollapsed = [], onSectionToggle, maxItemsInRecyclePool, drawDistance = DEFAULT_DRAW_DISTANCE, viewabilityConfig, onViewableItemsChanged, onEndReached, onEndReachedThreshold, style, contentContainerStyle, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, refreshing, onRefresh, extraData, debug = false,...scrollViewProps } = props;
1596
+ const scrollViewRef = (0, react.useRef)(null);
1597
+ const [viewportSize, setViewportSize] = (0, react.useState)({
1598
+ width: 0,
1599
+ height: 0
1600
+ });
1601
+ const [scrollOffset, setScrollOffset] = (0, react.useState)(0);
1602
+ const [collapsedSections, setCollapsedSections] = (0, react.useState)(() => new Set(defaultCollapsed));
1603
+ const layoutCache = (0, react.useMemo)(() => createLayoutCache(), []);
1604
+ const layoutManager = (0, react.useMemo)(() => createSectionLayoutManager(layoutCache, {
1605
+ horizontal,
1606
+ estimatedItemSize,
1607
+ estimatedHeaderSize: estimatedSectionHeaderSize,
1608
+ estimatedFooterSize: estimatedSectionFooterSize,
1609
+ hasSectionFooters: !!renderSectionFooter
1610
+ }), [
1611
+ layoutCache,
1612
+ horizontal,
1613
+ estimatedItemSize,
1614
+ estimatedSectionHeaderSize,
1615
+ estimatedSectionFooterSize,
1616
+ renderSectionFooter
1617
+ ]);
1618
+ const flattenedData = (0, react.useMemo)(() => layoutManager.updateData(sections, collapsedSections), [
1619
+ layoutManager,
1620
+ sections,
1621
+ collapsedSections,
1622
+ extraData
1623
+ ]);
1624
+ (0, react.useEffect)(() => {
1625
+ layoutManager.getLayoutPositioner().setContainerSize(viewportSize.width, viewportSize.height);
1626
+ }, [layoutManager, viewportSize]);
1627
+ const getItemTypeForIndex = (0, react.useCallback)((flatIndex) => {
1628
+ const item = flattenedData[flatIndex];
1629
+ if (!item) return DEFAULT_ITEM_TYPE;
1630
+ if (item.type === "section-header") return SECTION_HEADER_TYPE;
1631
+ if (item.type === "section-footer") return SECTION_FOOTER_TYPE;
1632
+ if (getItemType && item.item !== null) return getItemType(item.item, item.itemIndex, item.section);
1633
+ return DEFAULT_ITEM_TYPE;
1634
+ }, [flattenedData, getItemType]);
1635
+ const { scrollState, onScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd } = useScrollHandler({
1636
+ horizontal,
1637
+ onScrollStateChange: (state) => {
1638
+ setScrollOffset(state.offset);
1639
+ },
1640
+ onEndReached: onEndReached ? (distance) => onEndReached({ distanceFromEnd: distance }) : void 0,
1641
+ onEndReachedThreshold
1642
+ });
1643
+ const { getCell, releaseCell, updateVisibleRange, clearPools, getPoolStats } = useRecycler({
1644
+ flattenedData,
1645
+ getItemType,
1646
+ maxPoolSize: maxItemsInRecyclePool
1647
+ });
1648
+ const layoutPositioner = layoutManager.getLayoutPositioner();
1649
+ const viewportDimension = horizontal ? viewportSize.width : viewportSize.height;
1650
+ const visibleRange = (0, react.useMemo)(() => layoutPositioner.getVisibleRange(scrollOffset, viewportDimension, drawDistance), [
1651
+ layoutPositioner,
1652
+ scrollOffset,
1653
+ viewportDimension,
1654
+ drawDistance
1655
+ ]);
1656
+ (0, react.useEffect)(() => {
1657
+ updateVisibleRange(visibleRange.startIndex, visibleRange.endIndex);
1658
+ }, [
1659
+ updateVisibleRange,
1660
+ visibleRange.startIndex,
1661
+ visibleRange.endIndex
1662
+ ]);
1663
+ const sectionLayouts = (0, react.useMemo)(() => layoutManager.getAllSectionLayouts(), [
1664
+ layoutManager,
1665
+ flattenedData,
1666
+ scrollOffset
1667
+ ]);
1668
+ const stickyHeaderState = useStickyHeader({
1669
+ sectionLayouts,
1670
+ scrollOffset,
1671
+ viewportHeight: viewportDimension,
1672
+ horizontal,
1673
+ enabled: stickySectionHeadersEnabled
1674
+ });
1675
+ const viewabilityResult = useViewability({
1676
+ flattenedData,
1677
+ layoutPositioner,
1678
+ scrollOffset,
1679
+ viewportSize: viewportDimension,
1680
+ horizontal,
1681
+ viewabilityConfig,
1682
+ onViewableItemsChanged
1683
+ });
1684
+ const handleCellLayout = (0, react.useCallback)((key, flatIndex, layout) => {
1685
+ layoutPositioner.updateItemLayout(flatIndex, layout);
1686
+ }, [layoutPositioner]);
1687
+ const handleViewportLayout = (0, react.useCallback)((event) => {
1688
+ const { width, height } = event.nativeEvent.layout;
1689
+ setViewportSize({
1690
+ width,
1691
+ height
1692
+ });
1693
+ }, []);
1694
+ const handleSectionToggle = (0, react.useCallback)((sectionKey) => {
1695
+ setCollapsedSections((prev) => {
1696
+ const next = new Set(prev);
1697
+ const isCollapsed = next.has(sectionKey);
1698
+ if (isCollapsed) next.delete(sectionKey);
1699
+ else next.add(sectionKey);
1700
+ onSectionToggle?.(sectionKey, !isCollapsed);
1701
+ return next;
1702
+ });
1703
+ }, [onSectionToggle]);
1704
+ const contentSize = (0, react.useMemo)(() => layoutPositioner.getContentSize(), [layoutPositioner, flattenedData]);
1705
+ const scrollToOffset = (0, react.useCallback)((options) => {
1706
+ scrollViewRef.current?.scrollTo({
1707
+ x: horizontal ? options.offset : 0,
1708
+ y: horizontal ? 0 : options.offset,
1709
+ animated: options.animated ?? true
1710
+ });
1711
+ }, [horizontal]);
1712
+ const scrollToSection = (0, react.useCallback)((options) => {
1713
+ const { sectionKey, sectionIndex, animated = true, viewPosition = 0 } = options;
1714
+ let targetSection = null;
1715
+ if (sectionKey) targetSection = layoutManager.getSectionLayout(sectionKey);
1716
+ else if (sectionIndex !== void 0) {
1717
+ const section = sections[sectionIndex];
1718
+ if (section) targetSection = layoutManager.getSectionLayout(section.key);
1719
+ }
1720
+ if (targetSection) {
1721
+ const headerOffset = horizontal ? targetSection.headerLayout.x : targetSection.headerLayout.y;
1722
+ scrollToOffset({
1723
+ offset: headerOffset,
1724
+ animated
1725
+ });
1726
+ }
1727
+ }, [
1728
+ layoutManager,
1729
+ sections,
1730
+ horizontal,
1731
+ scrollToOffset
1732
+ ]);
1733
+ const scrollToItem = (0, react.useCallback)((options) => {
1734
+ const { sectionKey, sectionIndex, itemIndex, animated = true } = options;
1735
+ let targetSectionIndex = sectionIndex;
1736
+ if (sectionKey) targetSectionIndex = sections.findIndex((s) => s.key === sectionKey);
1737
+ if (targetSectionIndex === void 0 || targetSectionIndex < 0) return;
1738
+ const flatIndex = layoutManager.getFlatIndex(targetSectionIndex, itemIndex);
1739
+ if (flatIndex < 0) return;
1740
+ const layout = layoutPositioner.getLayoutForIndex(flatIndex);
1741
+ const offset = horizontal ? layout.x : layout.y;
1742
+ scrollToOffset({
1743
+ offset,
1744
+ animated
1745
+ });
1746
+ }, [
1747
+ layoutManager,
1748
+ layoutPositioner,
1749
+ sections,
1750
+ horizontal,
1751
+ scrollToOffset
1752
+ ]);
1753
+ const scrollToEnd = (0, react.useCallback)((options) => {
1754
+ const totalSize = horizontal ? contentSize.width : contentSize.height;
1755
+ const offset = Math.max(0, totalSize - viewportDimension);
1756
+ scrollToOffset({
1757
+ offset,
1758
+ animated: options?.animated ?? true
1759
+ });
1760
+ }, [
1761
+ contentSize,
1762
+ viewportDimension,
1763
+ horizontal,
1764
+ scrollToOffset
1765
+ ]);
1766
+ (0, react.useImperativeHandle)(ref, () => ({
1767
+ scrollToSection,
1768
+ scrollToItem,
1769
+ scrollToOffset,
1770
+ scrollToEnd,
1771
+ toggleSection: handleSectionToggle,
1772
+ getSectionLayouts: () => sectionLayouts,
1773
+ getVisibleItems: viewabilityResult.getVisibleItems,
1774
+ recordInteraction: viewabilityResult.recordInteraction,
1775
+ flashScrollIndicators: () => scrollViewRef.current?.flashScrollIndicators()
1776
+ }), [
1777
+ scrollToSection,
1778
+ scrollToItem,
1779
+ scrollToOffset,
1780
+ scrollToEnd,
1781
+ handleSectionToggle,
1782
+ sectionLayouts,
1783
+ viewabilityResult
1784
+ ]);
1785
+ if (sections.length === 0 && ListEmptyComponent) {
1786
+ const EmptyComponent = typeof ListEmptyComponent === "function" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListEmptyComponent, {}) : ListEmptyComponent;
1787
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
1788
+ style: [styles$1.container, style],
1789
+ children: EmptyComponent
1790
+ });
1791
+ }
1792
+ const HeaderComponent = ListHeaderComponent ? typeof ListHeaderComponent === "function" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListHeaderComponent, {}) : ListHeaderComponent : null;
1793
+ const FooterComponent = ListFooterComponent ? typeof ListFooterComponent === "function" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListFooterComponent, {}) : ListFooterComponent : null;
1794
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
1795
+ style: [styles$1.container, style],
1796
+ onLayout: handleViewportLayout,
1797
+ children: [
1798
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.ScrollView, {
1799
+ ref: scrollViewRef,
1800
+ horizontal,
1801
+ scrollEventThrottle: 16,
1802
+ onScroll,
1803
+ onScrollBeginDrag,
1804
+ onScrollEndDrag,
1805
+ onMomentumScrollBegin,
1806
+ onMomentumScrollEnd,
1807
+ contentContainerStyle,
1808
+ refreshControl: onRefresh ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.RefreshControl, {
1809
+ refreshing: refreshing ?? false,
1810
+ onRefresh
1811
+ }) : void 0,
1812
+ ...scrollViewProps,
1813
+ children: [
1814
+ HeaderComponent,
1815
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionFlowProvider, {
1816
+ sections,
1817
+ flattenedData,
1818
+ layoutManager,
1819
+ layoutCache,
1820
+ horizontal,
1821
+ collapsedSections,
1822
+ scrollOffset,
1823
+ viewportWidth: viewportSize.width,
1824
+ viewportHeight: viewportSize.height,
1825
+ stickySectionHeadersEnabled,
1826
+ estimatedItemSize,
1827
+ drawDistance,
1828
+ debug,
1829
+ renderItem,
1830
+ renderSectionHeader,
1831
+ renderSectionFooter,
1832
+ onCellMeasured: handleCellLayout,
1833
+ onSectionToggle: collapsible ? handleSectionToggle : void 0,
1834
+ getItemType: getItemTypeForIndex,
1835
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecyclerContainer, {
1836
+ flattenedData,
1837
+ visibleRange,
1838
+ getLayoutForIndex: (index) => layoutPositioner.getLayoutForIndex(index),
1839
+ getCell,
1840
+ contentSize,
1841
+ renderItem,
1842
+ renderSectionHeader,
1843
+ renderSectionFooter,
1844
+ onCellLayout: handleCellLayout,
1845
+ horizontal,
1846
+ debug
1847
+ })
1848
+ }),
1849
+ FooterComponent
1850
+ ]
1851
+ }),
1852
+ stickySectionHeadersEnabled && renderSectionHeader && stickyHeaderState.sectionKey && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StickyHeaderContainer, {
1853
+ stickySection: sectionLayouts.find((s) => s.sectionKey === stickyHeaderState.sectionKey) ?? null,
1854
+ translateY: stickyHeaderState.translateY,
1855
+ sections,
1856
+ renderSectionHeader,
1857
+ horizontal,
1858
+ style: stickyHeaderStyle
1859
+ }),
1860
+ debug && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
1861
+ style: styles$1.debugOverlay,
1862
+ pointerEvents: "none"
1863
+ })
1864
+ ]
1865
+ });
1866
+ }
1867
+ const styles$1 = react_native.StyleSheet.create({
1868
+ container: { flex: 1 },
1869
+ debugOverlay: {
1870
+ position: "absolute",
1871
+ top: 0,
1872
+ left: 0,
1873
+ right: 0,
1874
+ bottom: 0
1875
+ }
1876
+ });
1877
+ /**
1878
+ * Export with proper generic typing.
1879
+ * Usage: <SectionFlow<MyItemType> sections={...} renderItem={...} />
1880
+ */
1881
+ const SectionFlow = (0, react.forwardRef)(SectionFlowInner);
1882
+ SectionFlow.displayName = "SectionFlow";
1883
+
1884
+ //#endregion
1885
+ //#region src/components/SectionHeader.tsx
1886
+ /**
1887
+ * Default section header component.
1888
+ * Can be overridden by providing renderSectionHeader prop to SectionFlow.
1889
+ */
1890
+ function SectionHeaderComponent({ section, sectionIndex, isCollapsed = false, collapsible = false, onToggle, renderContent, style }) {
1891
+ const handlePress = (0, react.useCallback)(() => {
1892
+ if (collapsible && onToggle) onToggle(section.key);
1893
+ }, [
1894
+ collapsible,
1895
+ onToggle,
1896
+ section.key
1897
+ ]);
1898
+ if (renderContent) {
1899
+ const content = renderContent({
1900
+ section,
1901
+ sectionIndex,
1902
+ isCollapsed
1903
+ });
1904
+ if (content) {
1905
+ if (collapsible) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.TouchableOpacity, {
1906
+ onPress: handlePress,
1907
+ activeOpacity: .7,
1908
+ children: content
1909
+ });
1910
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, { children: content });
1911
+ }
1912
+ }
1913
+ const Wrapper = collapsible ? react_native.TouchableOpacity : react_native.View;
1914
+ const wrapperProps = collapsible ? {
1915
+ onPress: handlePress,
1916
+ activeOpacity: .7
1917
+ } : {};
1918
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Wrapper, {
1919
+ ...wrapperProps,
1920
+ style: [styles.container, style],
1921
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
1922
+ style: styles.title,
1923
+ children: section.title ?? section.key
1924
+ }), collapsible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
1925
+ style: styles.indicator,
1926
+ children: isCollapsed ? "▸" : "▾"
1927
+ })]
1928
+ });
1929
+ }
1930
+ const styles = react_native.StyleSheet.create({
1931
+ container: {
1932
+ flexDirection: "row",
1933
+ alignItems: "center",
1934
+ justifyContent: "space-between",
1935
+ paddingHorizontal: 16,
1936
+ paddingVertical: 12,
1937
+ backgroundColor: "#f5f5f5",
1938
+ borderBottomWidth: react_native.StyleSheet.hairlineWidth,
1939
+ borderBottomColor: "#e0e0e0"
1940
+ },
1941
+ title: {
1942
+ fontSize: 14,
1943
+ fontWeight: "600",
1944
+ color: "#333",
1945
+ textTransform: "uppercase",
1946
+ letterSpacing: .5
1947
+ },
1948
+ indicator: {
1949
+ fontSize: 14,
1950
+ color: "#666"
1951
+ }
1952
+ });
1953
+ const SectionHeader = (0, react.memo)(SectionHeaderComponent);
1954
+
1955
+ //#endregion
1956
+ //#region src/hooks/useLayoutMeasurement.ts
1957
+ /**
1958
+ * Hook for synchronous layout measurement using New Architecture features.
1959
+ *
1960
+ * In React Native's New Architecture with Fabric, useLayoutEffect runs synchronously
1961
+ * before paint, allowing us to measure and correct layouts without visible glitches.
1962
+ *
1963
+ * This hook provides:
1964
+ * 1. A ref to attach to the View
1965
+ * 2. An onLayout callback for initial/resize measurements
1966
+ * 3. A measure function for on-demand measurement
1967
+ */
1968
+ function useLayoutMeasurement(options) {
1969
+ const { onMeasure, enabled = true } = options;
1970
+ const ref = (0, react.useRef)(null);
1971
+ const lastMeasurement = (0, react.useRef)(null);
1972
+ /**
1973
+ * Measure the view using measureInWindow for absolute positioning.
1974
+ * In New Architecture, this is synchronous when called in useLayoutEffect.
1975
+ */
1976
+ const measure = (0, react.useCallback)(() => {
1977
+ if (!enabled || !ref.current) return;
1978
+ ref.current.measureInWindow((x, y, width, height) => {
1979
+ const layout = {
1980
+ x,
1981
+ y,
1982
+ width,
1983
+ height
1984
+ };
1985
+ if (!lastMeasurement.current || lastMeasurement.current.width !== width || lastMeasurement.current.height !== height) {
1986
+ lastMeasurement.current = layout;
1987
+ onMeasure(layout);
1988
+ }
1989
+ });
1990
+ }, [enabled, onMeasure]);
1991
+ /**
1992
+ * Handle layout events from React Native's onLayout prop.
1993
+ * This fires on mount and whenever the layout changes.
1994
+ */
1995
+ const onLayout = (0, react.useCallback)((event) => {
1996
+ if (!enabled) return;
1997
+ const { x, y, width, height } = event.nativeEvent.layout;
1998
+ const layout = {
1999
+ x,
2000
+ y,
2001
+ width,
2002
+ height
2003
+ };
2004
+ if (!lastMeasurement.current || lastMeasurement.current.width !== width || lastMeasurement.current.height !== height) {
2005
+ lastMeasurement.current = layout;
2006
+ onMeasure(layout);
2007
+ }
2008
+ }, [enabled, onMeasure]);
2009
+ return {
2010
+ ref,
2011
+ onLayout,
2012
+ measure
2013
+ };
2014
+ }
2015
+ /**
2016
+ * Hook for measuring a cell's layout and reporting to the layout system.
2017
+ * Uses synchronous measurement in New Architecture for smooth corrections.
2018
+ */
2019
+ function useCellMeasurement(cellKey, onCellMeasured, enabled = true) {
2020
+ const handleMeasure = (0, react.useCallback)((layout) => {
2021
+ onCellMeasured(cellKey, layout);
2022
+ }, [cellKey, onCellMeasured]);
2023
+ return useLayoutMeasurement({
2024
+ onMeasure: handleMeasure,
2025
+ enabled
2026
+ });
2027
+ }
2028
+ /**
2029
+ * Hook for progressive rendering - measures initial items and expands.
2030
+ * Implements FlashList v2's progressive rendering strategy.
2031
+ */
2032
+ function useProgressiveRender(totalItems, initialCount, batchSize, onRenderCountChange) {
2033
+ const renderCount = (0, react.useRef)(Math.min(initialCount, totalItems));
2034
+ const measuredCount = (0, react.useRef)(0);
2035
+ const onItemMeasured = (0, react.useCallback)((index) => {
2036
+ measuredCount.current++;
2037
+ if (measuredCount.current >= renderCount.current && renderCount.current < totalItems) {
2038
+ const newCount = Math.min(renderCount.current + batchSize, totalItems);
2039
+ renderCount.current = newCount;
2040
+ onRenderCountChange(newCount);
2041
+ }
2042
+ }, [
2043
+ totalItems,
2044
+ batchSize,
2045
+ onRenderCountChange
2046
+ ]);
2047
+ return {
2048
+ renderCount: renderCount.current,
2049
+ onItemMeasured
2050
+ };
2051
+ }
2052
+
2053
+ //#endregion
2054
+ //#region src/utils/flattenSections.ts
2055
+ /**
2056
+ * Flatten sections into a single array of items for virtualization.
2057
+ * Each item includes metadata about its section and position.
2058
+ */
2059
+ function flattenSections(sections, options = {}) {
2060
+ const { collapsedSections = new Set(), includeSectionFooters = false } = options;
2061
+ const result = [];
2062
+ for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
2063
+ const section = sections[sectionIndex];
2064
+ const isCollapsed = collapsedSections.has(section.key);
2065
+ result.push({
2066
+ type: "section-header",
2067
+ key: `header-${section.key}`,
2068
+ sectionKey: section.key,
2069
+ sectionIndex,
2070
+ itemIndex: -1,
2071
+ item: null,
2072
+ section
2073
+ });
2074
+ if (!isCollapsed) {
2075
+ for (let itemIndex = 0; itemIndex < section.data.length; itemIndex++) result.push({
2076
+ type: "item",
2077
+ key: `item-${section.key}-${itemIndex}`,
2078
+ sectionKey: section.key,
2079
+ sectionIndex,
2080
+ itemIndex,
2081
+ item: section.data[itemIndex],
2082
+ section
2083
+ });
2084
+ if (includeSectionFooters) result.push({
2085
+ type: "section-footer",
2086
+ key: `footer-${section.key}`,
2087
+ sectionKey: section.key,
2088
+ sectionIndex,
2089
+ itemIndex: -1,
2090
+ item: null,
2091
+ section
2092
+ });
2093
+ }
2094
+ }
2095
+ return result;
2096
+ }
2097
+ /**
2098
+ * Get the flat index for a specific section and item index.
2099
+ */
2100
+ function getFlatIndex(sections, sectionIndex, itemIndex, collapsedSections = new Set(), includeSectionFooters = false) {
2101
+ let flatIndex = 0;
2102
+ for (let i = 0; i < sectionIndex; i++) {
2103
+ const section = sections[i];
2104
+ flatIndex++;
2105
+ if (!collapsedSections.has(section.key)) {
2106
+ flatIndex += section.data.length;
2107
+ if (includeSectionFooters) flatIndex++;
2108
+ }
2109
+ }
2110
+ flatIndex++;
2111
+ if (itemIndex >= 0 && !collapsedSections.has(sections[sectionIndex].key)) flatIndex += itemIndex;
2112
+ return flatIndex;
2113
+ }
2114
+ /**
2115
+ * Get section and item index from a flat index.
2116
+ */
2117
+ function getSectionItemFromFlatIndex(sections, flatIndex, collapsedSections = new Set(), includeSectionFooters = false) {
2118
+ let currentFlatIndex = 0;
2119
+ for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
2120
+ const section = sections[sectionIndex];
2121
+ const isCollapsed = collapsedSections.has(section.key);
2122
+ if (currentFlatIndex === flatIndex) return {
2123
+ sectionIndex,
2124
+ itemIndex: -1,
2125
+ type: "header"
2126
+ };
2127
+ currentFlatIndex++;
2128
+ if (!isCollapsed) {
2129
+ for (let itemIndex = 0; itemIndex < section.data.length; itemIndex++) {
2130
+ if (currentFlatIndex === flatIndex) return {
2131
+ sectionIndex,
2132
+ itemIndex,
2133
+ type: "item"
2134
+ };
2135
+ currentFlatIndex++;
2136
+ }
2137
+ if (includeSectionFooters) {
2138
+ if (currentFlatIndex === flatIndex) return {
2139
+ sectionIndex,
2140
+ itemIndex: -1,
2141
+ type: "footer"
2142
+ };
2143
+ currentFlatIndex++;
2144
+ }
2145
+ }
2146
+ }
2147
+ return null;
2148
+ }
2149
+
2150
+ //#endregion
2151
+ //#region src/utils/keyExtractor.ts
2152
+ /**
2153
+ * Key extraction utilities for list items.
2154
+ */
2155
+ /**
2156
+ * Default key extractor that uses index as string.
2157
+ * Note: Using indices as keys is not recommended for dynamic lists.
2158
+ */
2159
+ function defaultKeyExtractor(item, index) {
2160
+ const anyItem = item;
2161
+ if (typeof anyItem?.id === "string" || typeof anyItem?.id === "number") return String(anyItem.id);
2162
+ if (typeof anyItem?.key === "string" || typeof anyItem?.key === "number") return String(anyItem.key);
2163
+ if (typeof anyItem?._id === "string" || typeof anyItem?._id === "number") return String(anyItem._id);
2164
+ if (typeof anyItem?.uuid === "string") return anyItem.uuid;
2165
+ return String(index);
2166
+ }
2167
+ /**
2168
+ * Create a key extractor function with a custom field name.
2169
+ */
2170
+ function createKeyExtractor(field) {
2171
+ return (item) => {
2172
+ const value = item[field];
2173
+ if (value !== void 0 && value !== null) return String(value);
2174
+ throw new Error(`Key field "${String(field)}" not found on item`);
2175
+ };
2176
+ }
2177
+ /**
2178
+ * Create a composite key from multiple fields.
2179
+ */
2180
+ function createCompositeKeyExtractor(fields) {
2181
+ return (item) => {
2182
+ return fields.map((field) => {
2183
+ const value = item[field];
2184
+ return value !== void 0 && value !== null ? String(value) : "";
2185
+ }).join("-");
2186
+ };
2187
+ }
2188
+ /**
2189
+ * Validate that keys are unique within a list.
2190
+ * Throws an error if duplicates are found.
2191
+ */
2192
+ function validateUniqueKeys(items, keyExtractor) {
2193
+ const seen = new Set();
2194
+ for (let i = 0; i < items.length; i++) {
2195
+ const key = keyExtractor(items[i], i);
2196
+ if (seen.has(key)) throw new Error(`Duplicate key "${key}" found at index ${i}. Keys must be unique within a list.`);
2197
+ seen.add(key);
2198
+ }
2199
+ }
2200
+
2201
+ //#endregion
2202
+ //#region src/utils/binarySearch.ts
2203
+ /**
2204
+ * Binary search utilities for efficient offset-to-index lookups.
2205
+ */
2206
+ /**
2207
+ * Binary search to find the index of an item with a specific value.
2208
+ * Returns -1 if not found.
2209
+ */
2210
+ function binarySearch(array, target, getValue) {
2211
+ let low = 0;
2212
+ let high = array.length - 1;
2213
+ while (low <= high) {
2214
+ const mid = Math.floor((low + high) / 2);
2215
+ const value = getValue(array[mid]);
2216
+ if (value === target) return mid;
2217
+ else if (value < target) low = mid + 1;
2218
+ else high = mid - 1;
2219
+ }
2220
+ return -1;
2221
+ }
2222
+ /**
2223
+ * Binary search to find the insertion position for a value.
2224
+ * Returns the index where the value should be inserted to maintain sorted order.
2225
+ */
2226
+ function binarySearchInsertPosition(array, target, getValue) {
2227
+ let low = 0;
2228
+ let high = array.length;
2229
+ while (low < high) {
2230
+ const mid = Math.floor((low + high) / 2);
2231
+ const value = getValue(array[mid]);
2232
+ if (value < target) low = mid + 1;
2233
+ else high = mid;
2234
+ }
2235
+ return low;
2236
+ }
2237
+
2238
+ //#endregion
2239
+ exports.AnimatedStickyHeaderContainer = AnimatedStickyHeaderContainer;
2240
+ exports.DEFAULT_DRAW_DISTANCE = DEFAULT_DRAW_DISTANCE;
2241
+ exports.DEFAULT_ESTIMATED_HEADER_SIZE = DEFAULT_ESTIMATED_HEADER_SIZE;
2242
+ exports.DEFAULT_ESTIMATED_ITEM_SIZE = DEFAULT_ESTIMATED_ITEM_SIZE;
2243
+ exports.DEFAULT_ITEM_TYPE = DEFAULT_ITEM_TYPE;
2244
+ exports.DEFAULT_MAX_POOL_SIZE = DEFAULT_MAX_POOL_SIZE;
2245
+ exports.LinearLayoutPositioner = LinearLayoutPositioner;
2246
+ exports.RecyclerCell = RecyclerCell;
2247
+ exports.RecyclerContainer = RecyclerContainer;
2248
+ exports.SECTION_FOOTER_TYPE = SECTION_FOOTER_TYPE;
2249
+ exports.SECTION_HEADER_TYPE = SECTION_HEADER_TYPE;
2250
+ exports.SectionFlow = SectionFlow;
2251
+ exports.SectionFlowProvider = SectionFlowProvider;
2252
+ exports.SectionHeader = SectionHeader;
2253
+ exports.StickyHeaderContainer = StickyHeaderContainer;
2254
+ exports.binarySearch = binarySearch;
2255
+ exports.binarySearchInsertPosition = binarySearchInsertPosition;
2256
+ exports.createCellRecycler = createCellRecycler;
2257
+ exports.createCompositeKeyExtractor = createCompositeKeyExtractor;
2258
+ exports.createKeyExtractor = createKeyExtractor;
2259
+ exports.createLayoutCache = createLayoutCache;
2260
+ exports.createLayoutPositioner = createLayoutPositioner;
2261
+ exports.createSectionLayoutManager = createSectionLayoutManager;
2262
+ exports.createViewabilityTracker = createViewabilityTracker;
2263
+ exports.defaultKeyExtractor = defaultKeyExtractor;
2264
+ exports.flattenSections = flattenSections;
2265
+ exports.getFlatIndex = getFlatIndex;
2266
+ exports.getSectionItemFromFlatIndex = getSectionItemFromFlatIndex;
2267
+ exports.useAdaptiveDrawDistance = useAdaptiveDrawDistance;
2268
+ exports.useCellMeasurement = useCellMeasurement;
2269
+ exports.useItemTypeResolver = useItemTypeResolver;
2270
+ exports.useLayoutMeasurement = useLayoutMeasurement;
2271
+ exports.useMultipleStickyHeaders = useMultipleStickyHeaders;
2272
+ exports.useMultipleViewabilityConfigs = useMultipleViewabilityConfigs;
2273
+ exports.useProgressiveRender = useProgressiveRender;
2274
+ exports.useRecycler = useRecycler;
2275
+ exports.useScrollHandler = useScrollHandler;
2276
+ exports.useSectionFlowContext = useSectionFlowContext;
2277
+ exports.useStickyHeader = useStickyHeader;
2278
+ exports.useStickyHeaderOpacity = useStickyHeaderOpacity;
2279
+ exports.useViewability = useViewability;
2280
+ exports.validateUniqueKeys = validateUniqueKeys;
2281
+ //# sourceMappingURL=index.cjs.map