@legendapp/list 3.0.0-beta.30 → 3.0.0-beta.32

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/index.native.js CHANGED
@@ -38,7 +38,7 @@ function getContentInsetEnd(state) {
38
38
  const baseInset = contentInset != null ? contentInset : state.nativeContentInset;
39
39
  const overrideInset = (_a3 = state.contentInsetOverride) != null ? _a3 : void 0;
40
40
  if (overrideInset) {
41
- const mergedInset = { bottom: 0, left: 0, right: 0, top: 0, ...baseInset, ...overrideInset };
41
+ const mergedInset = { bottom: 0, right: 0, ...baseInset, ...overrideInset };
42
42
  return (horizontal ? mergedInset.right : mergedInset.bottom) || 0;
43
43
  }
44
44
  if (baseInset) {
@@ -187,7 +187,7 @@ function useSelector$(signalName, selector) {
187
187
  var DebugRow = ({ children }) => {
188
188
  return /* @__PURE__ */ React2__namespace.createElement(View, { style: { alignItems: "center", flexDirection: "row", justifyContent: "space-between" } }, children);
189
189
  };
190
- var DebugView = React2__namespace.memo(function DebugView2({ state }) {
190
+ React2__namespace.memo(function DebugView2({ state }) {
191
191
  const ctx = useStateContext();
192
192
  const [totalSize = 0, scrollAdjust = 0, rawScroll = 0, scroll = 0, _numContainers = 0, _numContainersPooled = 0] = useArr$([
193
193
  "totalSize",
@@ -638,6 +638,11 @@ function useOnLayoutSync({
638
638
  var Platform2 = reactNative.Platform;
639
639
  var PlatformAdjustBreaksScroll = Platform2.OS === "android";
640
640
 
641
+ // src/utils/isInMVCPActiveMode.native.ts
642
+ function isInMVCPActiveMode(state) {
643
+ return state.dataChangeNeedsScrollUpdate;
644
+ }
645
+
641
646
  // src/components/Container.tsx
642
647
  var Container = typedMemo(function Container2({
643
648
  id,
@@ -746,7 +751,8 @@ var Container = typedMemo(function Container2({
746
751
  updateItemSizeFn(currentItemKey, layout);
747
752
  itemLayoutRef.current.didLayout = true;
748
753
  };
749
- if (Platform2.OS === "web" && prevSize !== void 0 && size + 1 < prevSize) {
754
+ const shouldDeferWebShrinkLayoutUpdate = Platform2.OS === "web" && !isInMVCPActiveMode(ctx.state) && prevSize !== void 0 && size + 1 < prevSize;
755
+ if (shouldDeferWebShrinkLayoutUpdate) {
750
756
  const token = pendingShrinkToken + 1;
751
757
  itemLayoutRef.current.pendingShrinkToken = token;
752
758
  requestAnimationFrame(() => {
@@ -775,8 +781,7 @@ var Container = typedMemo(function Container2({
775
781
  const { onLayout } = useOnLayoutSync(
776
782
  {
777
783
  onLayoutChange,
778
- ref
779
- },
784
+ ref},
780
785
  [itemKey, layoutRenderCount]
781
786
  );
782
787
  if (!IsNewArchitecture) {
@@ -884,25 +889,6 @@ var Containers = typedMemo(function Containers2({
884
889
  }
885
890
  return /* @__PURE__ */ React2__namespace.createElement(reactNative.Animated.View, { style }, containers);
886
891
  });
887
- function DevNumbers() {
888
- return IS_DEV && // biome-ignore lint/nursery/noShadow: const function name shadowing is intentional
889
- React2__namespace.memo(function DevNumbers2() {
890
- return Array.from({ length: 100 }).map((_, index) => /* @__PURE__ */ React2__namespace.createElement(
891
- reactNative.View,
892
- {
893
- key: index,
894
- style: {
895
- height: 100,
896
- pointerEvents: "none",
897
- position: "absolute",
898
- top: index * 100,
899
- width: "100%"
900
- }
901
- },
902
- /* @__PURE__ */ React2__namespace.createElement(reactNative.Text, { style: { color: "red" } }, index * 100)
903
- ));
904
- });
905
- }
906
892
  var ListComponentScrollView = reactNative.Animated.ScrollView;
907
893
  function ScrollAdjust() {
908
894
  const bias = 1e7;
@@ -1020,7 +1006,7 @@ var ListComponent = typedMemo(function ListComponent2({
1020
1006
  },
1021
1007
  getComponent(ListFooterComponent)
1022
1008
  ),
1023
- IS_DEV && ENABLE_DEVMODE && /* @__PURE__ */ React2__namespace.createElement(DevNumbers, null)
1009
+ IS_DEV && ENABLE_DEVMODE
1024
1010
  );
1025
1011
  });
1026
1012
 
@@ -1168,6 +1154,132 @@ function clampScrollOffset(ctx, offset) {
1168
1154
  return clampedOffset;
1169
1155
  }
1170
1156
 
1157
+ // src/utils/checkThreshold.ts
1158
+ var HYSTERESIS_MULTIPLIER = 1.3;
1159
+ var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot, allowReentryOnChange) => {
1160
+ const absDistance = Math.abs(distance);
1161
+ const within = atThreshold || threshold > 0 && absDistance <= threshold;
1162
+ const updateSnapshot = () => {
1163
+ setSnapshot({
1164
+ atThreshold,
1165
+ contentSize: context.contentSize,
1166
+ dataLength: context.dataLength,
1167
+ scrollPosition: context.scrollPosition
1168
+ });
1169
+ };
1170
+ if (!wasReached) {
1171
+ if (!within) {
1172
+ return false;
1173
+ }
1174
+ onReached(distance);
1175
+ updateSnapshot();
1176
+ return true;
1177
+ }
1178
+ const reset = !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
1179
+ if (reset) {
1180
+ setSnapshot(void 0);
1181
+ return false;
1182
+ }
1183
+ if (within) {
1184
+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
1185
+ if (changed) {
1186
+ if (allowReentryOnChange) {
1187
+ onReached(distance);
1188
+ }
1189
+ updateSnapshot();
1190
+ }
1191
+ }
1192
+ return true;
1193
+ };
1194
+
1195
+ // src/utils/checkAtBottom.ts
1196
+ function checkAtBottom(ctx) {
1197
+ var _a3;
1198
+ const state = ctx.state;
1199
+ if (!state || state.initialScroll) {
1200
+ return;
1201
+ }
1202
+ const {
1203
+ queuedInitialLayout,
1204
+ scrollLength,
1205
+ scroll,
1206
+ maintainingScrollAtEnd,
1207
+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
1208
+ } = state;
1209
+ if (state.initialScroll) {
1210
+ return;
1211
+ }
1212
+ const contentSize = getContentSize(ctx);
1213
+ if (contentSize > 0 && queuedInitialLayout && !maintainingScrollAtEnd) {
1214
+ const insetEnd = getContentInsetEnd(state);
1215
+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
1216
+ const isContentLess = contentSize < scrollLength;
1217
+ state.isAtEnd = isContentLess || distanceFromEnd < scrollLength * maintainScrollAtEndThreshold;
1218
+ state.isEndReached = checkThreshold(
1219
+ distanceFromEnd,
1220
+ isContentLess,
1221
+ onEndReachedThreshold * scrollLength,
1222
+ state.isEndReached,
1223
+ state.endReachedSnapshot,
1224
+ {
1225
+ contentSize,
1226
+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
1227
+ scrollPosition: scroll
1228
+ },
1229
+ (distance) => {
1230
+ var _a4, _b;
1231
+ return (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
1232
+ },
1233
+ (snapshot) => {
1234
+ state.endReachedSnapshot = snapshot;
1235
+ },
1236
+ true
1237
+ );
1238
+ }
1239
+ }
1240
+
1241
+ // src/utils/checkAtTop.ts
1242
+ function checkAtTop(ctx) {
1243
+ var _a3;
1244
+ const state = ctx == null ? void 0 : ctx.state;
1245
+ if (!state || state.initialScroll) {
1246
+ return;
1247
+ }
1248
+ const {
1249
+ scrollLength,
1250
+ scroll,
1251
+ props: { onStartReachedThreshold }
1252
+ } = state;
1253
+ const distanceFromTop = scroll;
1254
+ state.isAtStart = distanceFromTop <= 0;
1255
+ state.isStartReached = checkThreshold(
1256
+ distanceFromTop,
1257
+ false,
1258
+ onStartReachedThreshold * scrollLength,
1259
+ state.isStartReached,
1260
+ state.startReachedSnapshot,
1261
+ {
1262
+ contentSize: state.totalSize,
1263
+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
1264
+ scrollPosition: scroll
1265
+ },
1266
+ (distance) => {
1267
+ var _a4, _b;
1268
+ return (_b = (_a4 = state.props).onStartReached) == null ? void 0 : _b.call(_a4, { distanceFromStart: distance });
1269
+ },
1270
+ (snapshot) => {
1271
+ state.startReachedSnapshot = snapshot;
1272
+ },
1273
+ false
1274
+ );
1275
+ }
1276
+
1277
+ // src/utils/checkThresholds.ts
1278
+ function checkThresholds(ctx) {
1279
+ checkAtBottom(ctx);
1280
+ checkAtTop(ctx);
1281
+ }
1282
+
1171
1283
  // src/utils/setInitialRenderState.ts
1172
1284
  function setInitialRenderState(ctx, {
1173
1285
  didLayout,
@@ -1205,6 +1317,7 @@ function finishScrollTo(ctx) {
1205
1317
  state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
1206
1318
  }
1207
1319
  setInitialRenderState(ctx, { didInitialScroll: true });
1320
+ checkThresholds(ctx);
1208
1321
  }
1209
1322
  }
1210
1323
 
@@ -1224,7 +1337,8 @@ function checkFinishedScrollFrame(ctx) {
1224
1337
  const diff1 = Math.abs(scroll - clampedTargetOffset);
1225
1338
  const diff2 = Math.abs(diff1 - adjust);
1226
1339
  const isNotOverscrolled = Math.abs(scroll - maxOffset) < 1;
1227
- if (isNotOverscrolled && (diff1 < 1 || diff2 < 1)) {
1340
+ const isAtTarget = diff1 < 1 || !scrollingTo.animated && diff2 < 1;
1341
+ if (isNotOverscrolled && isAtTarget) {
1228
1342
  finishScrollTo(ctx);
1229
1343
  }
1230
1344
  }
@@ -1293,7 +1407,7 @@ function scrollTo(ctx, params) {
1293
1407
  }
1294
1408
  state.scrollPending = offset;
1295
1409
  if (forceScroll || !isInitialScroll || Platform2.OS === "android") {
1296
- doScrollTo(ctx, { animated, horizontal, isInitialScroll, offset });
1410
+ doScrollTo(ctx, { animated, horizontal, offset });
1297
1411
  } else {
1298
1412
  state.scroll = offset;
1299
1413
  }
@@ -1304,128 +1418,6 @@ var flushSync = (fn) => {
1304
1418
  fn();
1305
1419
  };
1306
1420
 
1307
- // src/utils/checkThreshold.ts
1308
- var HYSTERESIS_MULTIPLIER = 1.3;
1309
- var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot, allowReentryOnChange) => {
1310
- const absDistance = Math.abs(distance);
1311
- const within = atThreshold || threshold > 0 && absDistance <= threshold;
1312
- if (wasReached === null) {
1313
- if (!within && distance >= 0) {
1314
- return false;
1315
- }
1316
- return null;
1317
- }
1318
- const updateSnapshot = () => {
1319
- setSnapshot({
1320
- atThreshold,
1321
- contentSize: context.contentSize,
1322
- dataLength: context.dataLength,
1323
- scrollPosition: context.scrollPosition
1324
- });
1325
- };
1326
- if (!wasReached) {
1327
- if (!within) {
1328
- return false;
1329
- }
1330
- onReached(distance);
1331
- updateSnapshot();
1332
- return true;
1333
- }
1334
- const reset = !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
1335
- if (reset) {
1336
- setSnapshot(void 0);
1337
- return false;
1338
- }
1339
- if (within) {
1340
- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
1341
- if (changed) {
1342
- if (allowReentryOnChange) {
1343
- onReached(distance);
1344
- }
1345
- updateSnapshot();
1346
- }
1347
- }
1348
- return true;
1349
- };
1350
-
1351
- // src/utils/checkAtBottom.ts
1352
- function checkAtBottom(ctx) {
1353
- var _a3;
1354
- const state = ctx.state;
1355
- if (!state) {
1356
- return;
1357
- }
1358
- const {
1359
- queuedInitialLayout,
1360
- scrollLength,
1361
- scroll,
1362
- maintainingScrollAtEnd,
1363
- props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
1364
- } = state;
1365
- const contentSize = getContentSize(ctx);
1366
- if (contentSize > 0 && queuedInitialLayout && !maintainingScrollAtEnd) {
1367
- const insetEnd = getContentInsetEnd(state);
1368
- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
1369
- const isContentLess = contentSize < scrollLength;
1370
- state.isAtEnd = isContentLess || distanceFromEnd < scrollLength * maintainScrollAtEndThreshold;
1371
- state.isEndReached = checkThreshold(
1372
- distanceFromEnd,
1373
- isContentLess,
1374
- onEndReachedThreshold * scrollLength,
1375
- state.isEndReached,
1376
- state.endReachedSnapshot,
1377
- {
1378
- contentSize,
1379
- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
1380
- scrollPosition: scroll
1381
- },
1382
- (distance) => {
1383
- var _a4, _b;
1384
- return (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
1385
- },
1386
- (snapshot) => {
1387
- state.endReachedSnapshot = snapshot;
1388
- },
1389
- true
1390
- );
1391
- }
1392
- }
1393
-
1394
- // src/utils/checkAtTop.ts
1395
- function checkAtTop(state) {
1396
- var _a3;
1397
- if (!state) {
1398
- return;
1399
- }
1400
- const {
1401
- scrollLength,
1402
- scroll,
1403
- props: { onStartReachedThreshold }
1404
- } = state;
1405
- const distanceFromTop = scroll;
1406
- state.isAtStart = distanceFromTop <= 0;
1407
- state.isStartReached = checkThreshold(
1408
- distanceFromTop,
1409
- false,
1410
- onStartReachedThreshold * scrollLength,
1411
- state.isStartReached,
1412
- state.startReachedSnapshot,
1413
- {
1414
- contentSize: state.totalSize,
1415
- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
1416
- scrollPosition: scroll
1417
- },
1418
- (distance) => {
1419
- var _a4, _b;
1420
- return (_b = (_a4 = state.props).onStartReached) == null ? void 0 : _b.call(_a4, { distanceFromStart: distance });
1421
- },
1422
- (snapshot) => {
1423
- state.startReachedSnapshot = snapshot;
1424
- },
1425
- false
1426
- );
1427
- }
1428
-
1429
1421
  // src/core/updateScroll.ts
1430
1422
  function updateScroll(ctx, newScroll, forceUpdate) {
1431
1423
  const state = ctx.state;
@@ -1463,7 +1455,8 @@ function updateScroll(ctx, newScroll, forceUpdate) {
1463
1455
  const scrollDelta = Math.abs(newScroll - prevScroll);
1464
1456
  const scrollLength = state.scrollLength;
1465
1457
  const lastCalculated = state.scrollLastCalculate;
1466
- const shouldUpdate = forceUpdate || state.dataChangeNeedsScrollUpdate || state.scrollLastCalculate === void 0 || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2;
1458
+ const useAggressiveItemRecalculation = isInMVCPActiveMode(state);
1459
+ const shouldUpdate = useAggressiveItemRecalculation || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2;
1467
1460
  if (shouldUpdate) {
1468
1461
  state.scrollLastCalculate = state.scroll;
1469
1462
  state.ignoreScrollFromMVCPIgnored = false;
@@ -1471,8 +1464,7 @@ function updateScroll(ctx, newScroll, forceUpdate) {
1471
1464
  const runCalculateItems = () => {
1472
1465
  var _a3;
1473
1466
  (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state, { doMVCP: scrollingTo !== void 0 });
1474
- checkAtBottom(ctx);
1475
- checkAtTop(state);
1467
+ checkThresholds(ctx);
1476
1468
  };
1477
1469
  if (Platform2.OS === "web" && scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength) {
1478
1470
  flushSync(runCalculateItems);
@@ -1590,13 +1582,59 @@ function ensureInitialAnchor(ctx) {
1590
1582
  }
1591
1583
 
1592
1584
  // src/core/mvcp.ts
1585
+ var MVCP_POSITION_EPSILON = 0.1;
1586
+ var MVCP_ANCHOR_LOCK_TTL_MS = 300;
1587
+ var MVCP_ANCHOR_LOCK_QUIET_PASSES_TO_RELEASE = 2;
1588
+ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) {
1589
+ if (!enableMVCPAnchorLock) {
1590
+ state.mvcpAnchorLock = void 0;
1591
+ return void 0;
1592
+ }
1593
+ const lock = state.mvcpAnchorLock;
1594
+ if (!lock) {
1595
+ return void 0;
1596
+ }
1597
+ const isExpired = now > lock.expiresAt;
1598
+ const isMissing = state.indexByKey.get(lock.id) === void 0;
1599
+ if (isExpired || isMissing || !mvcpData) {
1600
+ state.mvcpAnchorLock = void 0;
1601
+ return void 0;
1602
+ }
1603
+ return lock;
1604
+ }
1605
+ function updateAnchorLock(state, params) {
1606
+ if (Platform2.OS === "web") {
1607
+ const { anchorId, anchorPosition, dataChanged, now, positionDiff } = params;
1608
+ const enableMVCPAnchorLock = !!dataChanged || !!state.mvcpAnchorLock;
1609
+ const mvcpData = state.props.maintainVisibleContentPosition.data;
1610
+ if (!enableMVCPAnchorLock || !mvcpData || state.scrollingTo || !anchorId || anchorPosition === void 0) {
1611
+ return;
1612
+ }
1613
+ const existingLock = state.mvcpAnchorLock;
1614
+ const quietPasses = !dataChanged && Math.abs(positionDiff) <= MVCP_POSITION_EPSILON && (existingLock == null ? void 0 : existingLock.id) === anchorId ? existingLock.quietPasses + 1 : 0;
1615
+ if (!dataChanged && quietPasses >= MVCP_ANCHOR_LOCK_QUIET_PASSES_TO_RELEASE) {
1616
+ state.mvcpAnchorLock = void 0;
1617
+ return;
1618
+ }
1619
+ state.mvcpAnchorLock = {
1620
+ expiresAt: now + MVCP_ANCHOR_LOCK_TTL_MS,
1621
+ id: anchorId,
1622
+ position: anchorPosition,
1623
+ quietPasses
1624
+ };
1625
+ }
1626
+ }
1593
1627
  function prepareMVCP(ctx, dataChanged) {
1594
1628
  const state = ctx.state;
1595
1629
  const { idsInView, positions, props } = state;
1596
1630
  const {
1597
1631
  maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition }
1598
1632
  } = props;
1633
+ const isWeb = Platform2.OS === "web";
1634
+ const now = Date.now();
1635
+ const enableMVCPAnchorLock = isWeb && (!!dataChanged || !!state.mvcpAnchorLock);
1599
1636
  const scrollingTo = state.scrollingTo;
1637
+ const anchorLock = isWeb ? resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) : void 0;
1600
1638
  let prevPosition;
1601
1639
  let targetId;
1602
1640
  const idsInViewWithPositions = [];
@@ -1605,31 +1643,48 @@ function prepareMVCP(ctx, dataChanged) {
1605
1643
  const shouldMVCP = dataChanged ? mvcpData : mvcpScroll;
1606
1644
  const indexByKey = state.indexByKey;
1607
1645
  if (shouldMVCP) {
1608
- if (scrollTarget !== void 0) {
1646
+ if (anchorLock && scrollTarget === void 0) {
1647
+ targetId = anchorLock.id;
1648
+ prevPosition = anchorLock.position;
1649
+ } else if (scrollTarget !== void 0) {
1609
1650
  if (!IsNewArchitecture && (scrollingTo == null ? void 0 : scrollingTo.isInitialScroll)) {
1610
1651
  return void 0;
1611
1652
  }
1612
1653
  targetId = getId(state, scrollTarget);
1613
- } else if (idsInView.length > 0 && state.didContainersLayout) {
1614
- if (dataChanged) {
1615
- for (let i = 0; i < idsInView.length; i++) {
1616
- const id = idsInView[i];
1617
- const index = indexByKey.get(id);
1618
- if (index !== void 0) {
1619
- idsInViewWithPositions.push({ id, position: positions.get(id) });
1620
- }
1654
+ } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) {
1655
+ targetId = idsInView.find((id) => indexByKey.get(id) !== void 0);
1656
+ }
1657
+ if (dataChanged && idsInView.length > 0 && state.didContainersLayout) {
1658
+ for (let i = 0; i < idsInView.length; i++) {
1659
+ const id = idsInView[i];
1660
+ const index = indexByKey.get(id);
1661
+ if (index !== void 0) {
1662
+ idsInViewWithPositions.push({ id, position: positions.get(id) });
1621
1663
  }
1622
- } else {
1623
- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0);
1624
1664
  }
1625
1665
  }
1626
- if (targetId !== void 0) {
1666
+ if (targetId !== void 0 && prevPosition === void 0) {
1627
1667
  prevPosition = positions.get(targetId);
1628
1668
  }
1629
1669
  return () => {
1630
1670
  let positionDiff = 0;
1631
- if (dataChanged && targetId === void 0 && mvcpData) {
1632
- const data = state.props.data;
1671
+ let anchorIdForLock = anchorLock == null ? void 0 : anchorLock.id;
1672
+ let anchorPositionForLock;
1673
+ let skipTargetAnchor = false;
1674
+ const data = state.props.data;
1675
+ const shouldValidateLockedAnchor = isWeb && dataChanged && mvcpData && scrollTarget === void 0 && targetId !== void 0 && (anchorLock == null ? void 0 : anchorLock.id) === targetId && shouldRestorePosition !== void 0;
1676
+ if (shouldValidateLockedAnchor && targetId !== void 0) {
1677
+ const index = indexByKey.get(targetId);
1678
+ if (index !== void 0) {
1679
+ const item = data[index];
1680
+ skipTargetAnchor = item === void 0 || !shouldRestorePosition(item, index, data);
1681
+ if (skipTargetAnchor && (anchorLock == null ? void 0 : anchorLock.id) === targetId) {
1682
+ state.mvcpAnchorLock = void 0;
1683
+ }
1684
+ }
1685
+ }
1686
+ const shouldUseFallbackVisibleAnchor = dataChanged && mvcpData && scrollTarget === void 0 && (targetId === void 0 || positions.get(targetId) === void 0 || skipTargetAnchor);
1687
+ if (shouldUseFallbackVisibleAnchor) {
1633
1688
  for (let i = 0; i < idsInViewWithPositions.length; i++) {
1634
1689
  const { id, position } = idsInViewWithPositions[i];
1635
1690
  const index = indexByKey.get(id);
@@ -1642,11 +1697,13 @@ function prepareMVCP(ctx, dataChanged) {
1642
1697
  const newPosition = positions.get(id);
1643
1698
  if (newPosition !== void 0) {
1644
1699
  positionDiff = newPosition - position;
1700
+ anchorIdForLock = id;
1701
+ anchorPositionForLock = newPosition;
1645
1702
  break;
1646
1703
  }
1647
1704
  }
1648
1705
  }
1649
- if (targetId !== void 0 && prevPosition !== void 0) {
1706
+ if (!skipTargetAnchor && targetId !== void 0 && prevPosition !== void 0) {
1650
1707
  const newPosition = positions.get(targetId);
1651
1708
  if (newPosition !== void 0) {
1652
1709
  const totalSize = getContentSize(ctx);
@@ -1659,20 +1716,29 @@ function prepareMVCP(ctx, dataChanged) {
1659
1716
  }
1660
1717
  }
1661
1718
  positionDiff = diff;
1719
+ anchorIdForLock = targetId;
1720
+ anchorPositionForLock = newPosition;
1662
1721
  }
1663
1722
  }
1664
1723
  if (scrollingToViewPosition && scrollingToViewPosition > 0) {
1665
1724
  const newSize = getItemSize(ctx, targetId, scrollTarget, state.props.data[scrollTarget]);
1666
1725
  const prevSize = scrollingTo == null ? void 0 : scrollingTo.itemSize;
1667
- if (newSize !== void 0 && prevSize !== void 0 && newSize !== (scrollingTo == null ? void 0 : scrollingTo.itemSize)) {
1726
+ if (newSize !== void 0 && prevSize !== void 0 && newSize !== prevSize) {
1668
1727
  const diff = newSize - prevSize;
1669
1728
  if (diff !== 0) {
1670
- positionDiff += (newSize - prevSize) * scrollingToViewPosition;
1729
+ positionDiff += diff * scrollingToViewPosition;
1671
1730
  scrollingTo.itemSize = newSize;
1672
1731
  }
1673
1732
  }
1674
1733
  }
1675
- if (Math.abs(positionDiff) > 0.1) {
1734
+ updateAnchorLock(state, {
1735
+ anchorId: anchorIdForLock,
1736
+ anchorPosition: anchorPositionForLock,
1737
+ dataChanged,
1738
+ now,
1739
+ positionDiff
1740
+ });
1741
+ if (Math.abs(positionDiff) > MVCP_POSITION_EPSILON) {
1676
1742
  requestAdjust(ctx, positionDiff, dataChanged && mvcpData);
1677
1743
  }
1678
1744
  };
@@ -2386,7 +2452,7 @@ function handleStickyActivation(ctx, stickyHeaderIndices, stickyArray, currentSt
2386
2452
  }
2387
2453
  }
2388
2454
  }
2389
- function handleStickyRecycling(ctx, stickyArray, scroll, scrollBuffer, currentStickyIdx, pendingRemoval, alwaysRenderIndicesSet) {
2455
+ function handleStickyRecycling(ctx, stickyArray, scroll, drawDistance, currentStickyIdx, pendingRemoval, alwaysRenderIndicesSet) {
2390
2456
  var _a3, _b, _c;
2391
2457
  const state = ctx.state;
2392
2458
  for (const containerIndex of state.stickyContainerPool) {
@@ -2407,13 +2473,13 @@ function handleStickyRecycling(ctx, stickyArray, scroll, scrollBuffer, currentSt
2407
2473
  if (nextIndex) {
2408
2474
  const nextId = (_a3 = state.idCache[nextIndex]) != null ? _a3 : getId(state, nextIndex);
2409
2475
  const nextPos = nextId ? state.positions.get(nextId) : void 0;
2410
- shouldRecycle = nextPos !== void 0 && scroll > nextPos + scrollBuffer * 2;
2476
+ shouldRecycle = nextPos !== void 0 && scroll > nextPos + drawDistance * 2;
2411
2477
  } else {
2412
2478
  const currentId = (_b = state.idCache[itemIndex]) != null ? _b : getId(state, itemIndex);
2413
2479
  if (currentId) {
2414
2480
  const currentPos = state.positions.get(currentId);
2415
2481
  const currentSize = (_c = state.sizes.get(currentId)) != null ? _c : getItemSize(ctx, currentId, itemIndex, state.props.data[itemIndex]);
2416
- shouldRecycle = currentPos !== void 0 && scroll > currentPos + currentSize + scrollBuffer * 3;
2482
+ shouldRecycle = currentPos !== void 0 && scroll > currentPos + currentSize + drawDistance * 3;
2417
2483
  }
2418
2484
  }
2419
2485
  if (shouldRecycle) {
@@ -2438,11 +2504,11 @@ function calculateItemsInView(ctx, params = {}) {
2438
2504
  props: {
2439
2505
  alwaysRenderIndicesArr,
2440
2506
  alwaysRenderIndicesSet,
2507
+ drawDistance,
2441
2508
  getItemType,
2442
2509
  itemsAreEqual,
2443
2510
  keyExtractor,
2444
- onStickyHeaderChange,
2445
- scrollBuffer
2511
+ onStickyHeaderChange
2446
2512
  },
2447
2513
  scrollForNextCalculateItemsInView,
2448
2514
  scrollLength,
@@ -2455,6 +2521,7 @@ function calculateItemsInView(ctx, params = {}) {
2455
2521
  const stickyIndicesSet = state.props.stickyIndicesSet || /* @__PURE__ */ new Set();
2456
2522
  const alwaysRenderArr = alwaysRenderIndicesArr || [];
2457
2523
  const alwaysRenderSet = alwaysRenderIndicesSet || /* @__PURE__ */ new Set();
2524
+ const { dataChanged, doMVCP, forceFullItemPositions } = params;
2458
2525
  const prevNumContainers = peek$(ctx, "numContainers");
2459
2526
  if (!data || scrollLength === 0 || !prevNumContainers) {
2460
2527
  if (!IsNewArchitecture && state.initialAnchor) {
@@ -2465,7 +2532,6 @@ function calculateItemsInView(ctx, params = {}) {
2465
2532
  const totalSize = getContentSize(ctx);
2466
2533
  const topPad = peek$(ctx, "stylePaddingTop") + peek$(ctx, "headerSize");
2467
2534
  const numColumns = peek$(ctx, "numColumns");
2468
- const { dataChanged, doMVCP, forceFullItemPositions } = params;
2469
2535
  const speed = getScrollVelocity(state);
2470
2536
  const scrollExtra = 0;
2471
2537
  const { queuedInitialLayout } = state;
@@ -2484,24 +2550,20 @@ function calculateItemsInView(ctx, params = {}) {
2484
2550
  if (scroll + scrollLength > totalSize) {
2485
2551
  scroll = Math.max(0, totalSize - scrollLength);
2486
2552
  }
2487
- if (ENABLE_DEBUG_VIEW) {
2488
- set$(ctx, "debugRawScroll", scrollState);
2489
- set$(ctx, "debugComputedScroll", scroll);
2490
- }
2491
2553
  const previousStickyIndex = peek$(ctx, "activeStickyIndex");
2492
2554
  const currentStickyIdx = stickyIndicesArr.length > 0 ? findCurrentStickyIndex(stickyIndicesArr, scroll, state) : -1;
2493
2555
  const nextActiveStickyIndex = currentStickyIdx >= 0 ? stickyIndicesArr[currentStickyIdx] : -1;
2494
2556
  if (currentStickyIdx >= 0 || previousStickyIndex >= 0) {
2495
2557
  set$(ctx, "activeStickyIndex", nextActiveStickyIndex);
2496
2558
  }
2497
- let scrollBufferTop = scrollBuffer;
2498
- let scrollBufferBottom = scrollBuffer;
2499
- if (speed > 0 || speed === 0 && scroll < Math.max(50, scrollBuffer)) {
2500
- scrollBufferTop = scrollBuffer * 0.5;
2501
- scrollBufferBottom = scrollBuffer * 1.5;
2559
+ let scrollBufferTop = drawDistance;
2560
+ let scrollBufferBottom = drawDistance;
2561
+ if (speed > 0 || speed === 0 && scroll < Math.max(50, drawDistance)) {
2562
+ scrollBufferTop = drawDistance * 0.5;
2563
+ scrollBufferBottom = drawDistance * 1.5;
2502
2564
  } else {
2503
- scrollBufferTop = scrollBuffer * 1.5;
2504
- scrollBufferBottom = scrollBuffer * 0.5;
2565
+ scrollBufferTop = drawDistance * 1.5;
2566
+ scrollBufferBottom = drawDistance * 0.5;
2505
2567
  }
2506
2568
  const scrollTopBuffered = scroll - scrollBufferTop;
2507
2569
  const scrollBottom = scroll + scrollLength + (scroll < 0 ? -scroll : 0);
@@ -2514,7 +2576,9 @@ function calculateItemsInView(ctx, params = {}) {
2514
2576
  if (!IsNewArchitecture && state.initialAnchor) {
2515
2577
  ensureInitialAnchor(ctx);
2516
2578
  }
2517
- return;
2579
+ if (Platform2.OS !== "web" || !isInMVCPActiveMode(state)) {
2580
+ return;
2581
+ }
2518
2582
  }
2519
2583
  }
2520
2584
  const checkMVCP = doMVCP ? prepareMVCP(ctx, dataChanged) : void 0;
@@ -2746,7 +2810,7 @@ function calculateItemsInView(ctx, params = {}) {
2746
2810
  ctx,
2747
2811
  stickyIndicesArr,
2748
2812
  scroll,
2749
- scrollBuffer,
2813
+ drawDistance,
2750
2814
  currentStickyIdx,
2751
2815
  pendingRemoval,
2752
2816
  alwaysRenderSet
@@ -2946,8 +3010,7 @@ function checkResetContainers(ctx, dataProp) {
2946
3010
  state.isEndReached = false;
2947
3011
  }
2948
3012
  if (!didMaintainScrollAtEnd) {
2949
- checkAtTop(state);
2950
- checkAtBottom(ctx);
3013
+ checkThresholds(ctx);
2951
3014
  }
2952
3015
  delete state.previousData;
2953
3016
  }
@@ -2960,10 +3023,10 @@ function doInitialAllocateContainers(ctx) {
2960
3023
  scrollLength,
2961
3024
  props: {
2962
3025
  data,
3026
+ drawDistance,
2963
3027
  getEstimatedItemSize,
2964
3028
  getFixedItemSize,
2965
3029
  getItemType,
2966
- scrollBuffer,
2967
3030
  numColumns,
2968
3031
  estimatedItemSize
2969
3032
  }
@@ -2985,7 +3048,7 @@ function doInitialAllocateContainers(ctx) {
2985
3048
  } else {
2986
3049
  averageItemSize = estimatedItemSize;
2987
3050
  }
2988
- const numContainers = Math.ceil((scrollLength + scrollBuffer * 2) / averageItemSize * numColumns);
3051
+ const numContainers = Math.ceil((scrollLength + drawDistance * 2) / averageItemSize * numColumns);
2989
3052
  for (let i = 0; i < numContainers; i++) {
2990
3053
  set$(ctx, `containerPosition${i}`, POSITION_OUT_OF_VIEW);
2991
3054
  set$(ctx, `containerColumn${i}`, -1);
@@ -3035,8 +3098,7 @@ function handleLayout(ctx, layout, setCanRender) {
3035
3098
  if (maintainScrollAtEnd === true || maintainScrollAtEnd.onLayout) {
3036
3099
  doMaintainScrollAtEnd(ctx, false);
3037
3100
  }
3038
- checkAtBottom(ctx);
3039
- checkAtTop(state);
3101
+ checkThresholds(ctx);
3040
3102
  if (state) {
3041
3103
  state.needsOtherAxisSize = otherAxisSize - (state.props.stylePaddingTop || 0) < 10;
3042
3104
  }
@@ -3074,7 +3136,7 @@ function onScroll(ctx, event) {
3074
3136
  }
3075
3137
  }
3076
3138
  let newScroll = event.nativeEvent.contentOffset[state.props.horizontal ? "x" : "y"];
3077
- if (state.scrollingTo) {
3139
+ if (state.scrollingTo && state.scrollingTo.offset >= newScroll) {
3078
3140
  const maxOffset = clampScrollOffset(ctx, newScroll);
3079
3141
  if (newScroll !== maxOffset && Math.abs(newScroll - maxOffset) > 1) {
3080
3142
  newScroll = maxOffset;
@@ -3147,6 +3209,28 @@ var ScrollAdjustHandler = class {
3147
3209
  };
3148
3210
 
3149
3211
  // src/core/updateItemSize.ts
3212
+ function runOrScheduleMVCPRecalculate(ctx) {
3213
+ const state = ctx.state;
3214
+ if (Platform2.OS === "web") {
3215
+ if (!state.mvcpAnchorLock) {
3216
+ if (state.queuedMVCPRecalculate !== void 0) {
3217
+ cancelAnimationFrame(state.queuedMVCPRecalculate);
3218
+ state.queuedMVCPRecalculate = void 0;
3219
+ }
3220
+ calculateItemsInView(ctx, { doMVCP: true });
3221
+ return;
3222
+ }
3223
+ if (state.queuedMVCPRecalculate !== void 0) {
3224
+ return;
3225
+ }
3226
+ state.queuedMVCPRecalculate = requestAnimationFrame(() => {
3227
+ state.queuedMVCPRecalculate = void 0;
3228
+ calculateItemsInView(ctx, { doMVCP: true });
3229
+ });
3230
+ } else {
3231
+ calculateItemsInView(ctx, { doMVCP: true });
3232
+ }
3233
+ }
3150
3234
  function updateItemSize(ctx, itemKey, sizeObj) {
3151
3235
  var _a3;
3152
3236
  const state = ctx.state;
@@ -3230,7 +3314,7 @@ function updateItemSize(ctx, itemKey, sizeObj) {
3230
3314
  if (didContainersLayout || checkAllSizesKnown(state)) {
3231
3315
  if (needsRecalculate) {
3232
3316
  state.scrollForNextCalculateItemsInView = void 0;
3233
- calculateItemsInView(ctx, { doMVCP: true });
3317
+ runOrScheduleMVCPRecalculate(ctx);
3234
3318
  }
3235
3319
  if (shouldMaintainScrollAtEnd) {
3236
3320
  if (maintainScrollAtEnd === true || maintainScrollAtEnd.onItemLayout) {
@@ -3595,8 +3679,6 @@ function useThrottledOnScroll(originalHandler, scrollEventThrottle) {
3595
3679
  }
3596
3680
 
3597
3681
  // src/components/LegendList.tsx
3598
- var DEFAULT_DRAW_DISTANCE = 250;
3599
- var DEFAULT_ITEM_SIZE = 100;
3600
3682
  var LegendList = typedMemo(
3601
3683
  // biome-ignore lint/nursery/noShadow: const function name shadowing is intentional
3602
3684
  typedForwardRef(function LegendList2(props, forwardedRef) {
@@ -3626,7 +3708,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
3626
3708
  data: dataProp = [],
3627
3709
  dataVersion,
3628
3710
  drawDistance = 250,
3629
- estimatedItemSize: estimatedItemSizeProp,
3711
+ estimatedItemSize = 100,
3630
3712
  estimatedListSize,
3631
3713
  extraData,
3632
3714
  getEstimatedItemSize,
@@ -3710,9 +3792,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
3710
3792
  ctx.columnWrapperStyle = columnWrapperStyle || (contentContainerStyle ? createColumnWrapperStyle(contentContainerStyle) : void 0);
3711
3793
  const refScroller = React2.useRef(null);
3712
3794
  const combinedRef = useCombinedRef(refScroller, refScrollView);
3713
- const estimatedItemSize = estimatedItemSizeProp != null ? estimatedItemSizeProp : DEFAULT_ITEM_SIZE;
3714
- const scrollBuffer = (drawDistance != null ? drawDistance : DEFAULT_DRAW_DISTANCE) || 1;
3715
- const keyExtractor = keyExtractorProp != null ? keyExtractorProp : (_item, index) => index.toString();
3795
+ const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString());
3716
3796
  const stickyHeaderIndices = stickyHeaderIndicesProp != null ? stickyHeaderIndicesProp : stickyIndicesDeprecated;
3717
3797
  const alwaysRenderIndices = React2.useMemo(() => {
3718
3798
  const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor);
@@ -3741,8 +3821,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
3741
3821
  ctx.state = {
3742
3822
  activeStickyIndex: -1,
3743
3823
  averageSizes: {},
3744
- columns: /* @__PURE__ */ new Map(),
3745
3824
  columnSpans: /* @__PURE__ */ new Map(),
3825
+ columns: /* @__PURE__ */ new Map(),
3746
3826
  containerItemKeys: /* @__PURE__ */ new Map(),
3747
3827
  containerItemTypes: /* @__PURE__ */ new Map(),
3748
3828
  contentInsetOverride: void 0,
@@ -3829,6 +3909,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
3829
3909
  contentInset,
3830
3910
  data: dataProp,
3831
3911
  dataVersion,
3912
+ drawDistance,
3832
3913
  estimatedItemSize,
3833
3914
  getEstimatedItemSize: useWrapIfItem(getEstimatedItemSize),
3834
3915
  getFixedItemSize: useWrapIfItem(getFixedItemSize),
@@ -3852,7 +3933,6 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
3852
3933
  overrideItemLayout,
3853
3934
  recycleItems: !!recycleItems,
3854
3935
  renderItem,
3855
- scrollBuffer,
3856
3936
  snapToIndices,
3857
3937
  stickyIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [],
3858
3938
  stickyIndicesSet: React2.useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]),
@@ -4099,7 +4179,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
4099
4179
  updateItemSize: fns.updateItemSize,
4100
4180
  waitForInitialLayout
4101
4181
  }
4102
- ), IS_DEV && ENABLE_DEBUG_VIEW && /* @__PURE__ */ React2__namespace.createElement(DebugView, { state: refState.current }));
4182
+ ), IS_DEV && ENABLE_DEBUG_VIEW);
4103
4183
  });
4104
4184
 
4105
4185
  exports.LegendList = LegendList;