@ankhorage/zora 25.0.0 → 25.1.0

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/dist/features/explorer/adapters/inbound/Explorer.d.ts.map +1 -1
  4. package/dist/features/explorer/adapters/inbound/Explorer.js +30 -13
  5. package/dist/features/explorer/adapters/inbound/Explorer.js.map +1 -1
  6. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.d.ts +9 -0
  7. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.d.ts.map +1 -0
  8. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.js +5 -0
  9. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.js.map +1 -0
  10. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.d.ts +9 -0
  11. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.d.ts.map +1 -0
  12. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.js +5 -0
  13. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.js.map +1 -0
  14. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.d.ts +9 -0
  15. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.d.ts.map +1 -0
  16. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.js +10 -0
  17. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.js.map +1 -0
  18. package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.d.ts +14 -0
  19. package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.d.ts.map +1 -0
  20. package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.js +54 -0
  21. package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.js.map +1 -0
  22. package/dist/features/explorer/application/getExplorerNextFocusId.d.ts +6 -0
  23. package/dist/features/explorer/application/getExplorerNextFocusId.d.ts.map +1 -0
  24. package/dist/features/explorer/application/getExplorerNextFocusId.js +53 -0
  25. package/dist/features/explorer/application/getExplorerNextFocusId.js.map +1 -0
  26. package/dist/features/explorer/public.d.ts +1 -0
  27. package/dist/features/explorer/public.d.ts.map +1 -1
  28. package/dist/features/explorer/public.js +1 -0
  29. package/dist/features/explorer/public.js.map +1 -1
  30. package/dist/features/grid-view/adapters/inbound/GridView.d.ts +1 -1
  31. package/dist/features/grid-view/adapters/inbound/GridView.d.ts.map +1 -1
  32. package/dist/features/grid-view/adapters/inbound/GridView.js +36 -4
  33. package/dist/features/grid-view/adapters/inbound/GridView.js.map +1 -1
  34. package/dist/features/grid-view/adapters/inbound/TileGrid.d.ts +1 -1
  35. package/dist/features/grid-view/adapters/inbound/TileGrid.d.ts.map +1 -1
  36. package/dist/features/grid-view/adapters/inbound/TileGrid.js +7 -3
  37. package/dist/features/grid-view/adapters/inbound/TileGrid.js.map +1 -1
  38. package/dist/types/grid-view.d.ts +10 -0
  39. package/dist/types/grid-view.d.ts.map +1 -1
  40. package/dist/types/grid-view.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/features/explorer/adapters/inbound/Explorer.tsx +79 -43
  43. package/src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.tsx +11 -0
  44. package/src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.tsx +11 -0
  45. package/src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.tsx +20 -0
  46. package/src/features/explorer/adapters/inbound/useExplorerKeyboardFocus.test.tsx +232 -0
  47. package/src/features/explorer/adapters/inbound/useExplorerKeyboardFocus.ts +77 -0
  48. package/src/features/explorer/application/getExplorerNextFocusId.test.ts +37 -0
  49. package/src/features/explorer/application/getExplorerNextFocusId.ts +61 -0
  50. package/src/features/explorer/public.ts +1 -0
  51. package/src/features/grid-view/adapters/inbound/GridView.tsx +49 -1
  52. package/src/features/grid-view/adapters/inbound/TileGrid.tsx +11 -0
  53. package/src/types/grid-view.ts +10 -0
  54. package/web-dist/chunks/{Accordion-8m8p9wpw.js → Accordion-54k0ncs3.js} +83 -0
  55. package/web-dist/chunks/Accordion-fmn31fgf.js +321 -0
  56. package/web-dist/components/explorer/Explorer.js +2 -2
  57. package/web-dist/components/file-explorer/FileExplorer.js +2 -2
  58. package/web-dist/components/grid-view/GridView.js +1 -1
  59. package/web-dist/components/media-explorer/MediaExplorer.js +2 -2
  60. package/web-dist/components/tile-grid/TileGrid.js +1 -1
  61. package/web-dist/components/uploader/Uploader.js +2 -2
  62. package/web-dist/manifest.json +10 -10
  63. package/web-dist/chunks/Accordion-2gvs3frs.js +0 -197
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { getExplorerNextFocusId } from './getExplorerNextFocusId';
4
+
5
+ describe('Explorer virtualized keyboard focus', () => {
6
+ const ids = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
7
+
8
+ test('moves by tile and by responsive row while staying within the catalogue', () => {
9
+ expect(getExplorerNextFocusId(ids, 'b', 'ArrowRight', 3)).toBe('c');
10
+ expect(getExplorerNextFocusId(ids, 'b', 'ArrowDown', 3)).toBe('e');
11
+ expect(getExplorerNextFocusId(ids, 'b', 'ArrowUp', 3)).toBe('a');
12
+ expect(getExplorerNextFocusId(ids, 'g', 'ArrowDown', 3)).toBe('g');
13
+ });
14
+
15
+ test('uses full visual positions and skips disabled items without collapsing rows', () => {
16
+ const disabled = new Set(['d', 'a']);
17
+ expect(getExplorerNextFocusId(ids, 'b', 'ArrowDown', 3, disabled)).toBe('e');
18
+ expect(getExplorerNextFocusId(ids, 'b', 'Home', 3, disabled)).toBe('b');
19
+ expect(getExplorerNextFocusId(ids, 'f', 'ArrowLeft', 3, new Set(['e']))).toBe('d');
20
+ expect(getExplorerNextFocusId(ids, 'g', 'End', 3, new Set(['g']))).toBe('f');
21
+ expect(getExplorerNextFocusId(ids, 'b', 'Home', 3, new Set(ids))).toBeNull();
22
+ });
23
+
24
+ test('navigates across ten thousand virtual positions without materializing DOM cells', () => {
25
+ const large = Array.from({ length: 10000 }, (_, index) => `tile-${index}`);
26
+ expect(getExplorerNextFocusId(large, 'tile-0', 'End', 4)).toBe('tile-9999');
27
+ expect(getExplorerNextFocusId(large, 'tile-9999', 'Home', 4)).toBe('tile-0');
28
+ expect(getExplorerNextFocusId(large, 'tile-2', 'ArrowDown', 4)).toBe('tile-6');
29
+ expect(getExplorerNextFocusId(large, 'tile-2', 'ArrowDown', 2)).toBe('tile-4');
30
+ });
31
+
32
+ test('supports first and last collection navigation without creating placeholder cells', () => {
33
+ expect(getExplorerNextFocusId(ids, 'd', 'Home', 3)).toBe('a');
34
+ expect(getExplorerNextFocusId(ids, 'd', 'End', 3)).toBe('g');
35
+ expect(getExplorerNextFocusId(ids, 'd', 'Enter', 3)).toBeNull();
36
+ });
37
+ });
@@ -0,0 +1,61 @@
1
+ /***
2
+ * Resolve focus along actual tile positions while skipping unavailable items
3
+ * without collapsing the visual row geometry.
4
+ */
5
+ export function getExplorerNextFocusId(
6
+ ids: readonly string[],
7
+ currentId: string | null,
8
+ key: string,
9
+ columns: number,
10
+ disabledIds: ReadonlySet<string> = new Set(),
11
+ ): string | null {
12
+ const delta = resolveFocusDelta(key, columns);
13
+ if (delta === null || ids.length === 0) return null;
14
+
15
+ const firstIndex = ids.findIndex((id) => !disabledIds.has(id));
16
+ if (firstIndex < 0) return null;
17
+
18
+ const currentIndex = Math.max(firstIndex, ids.indexOf(currentId ?? ''));
19
+ const candidateIndex =
20
+ delta === Number.NEGATIVE_INFINITY
21
+ ? 0
22
+ : delta === Number.POSITIVE_INFINITY
23
+ ? ids.length - 1
24
+ : Math.max(0, Math.min(ids.length - 1, currentIndex + delta));
25
+ const direction =
26
+ delta === Number.NEGATIVE_INFINITY
27
+ ? 1
28
+ : delta === Number.POSITIVE_INFINITY
29
+ ? -1
30
+ : Math.sign(delta);
31
+
32
+ for (let index = candidateIndex; index >= 0 && index < ids.length; index += direction) {
33
+ const candidate = ids.at(index);
34
+ if (candidate !== undefined && !disabledIds.has(candidate)) return candidate;
35
+ }
36
+ const current = ids.at(currentIndex);
37
+ return current !== undefined && !disabledIds.has(current)
38
+ ? current
39
+ : (ids.at(firstIndex) ?? null);
40
+ }
41
+
42
+ /*** Translate navigation keys into collection-relative movements. */
43
+ function resolveFocusDelta(key: string, columns: number): number | null {
44
+ const rowSize = Math.max(1, Math.floor(columns));
45
+ switch (key) {
46
+ case 'ArrowLeft':
47
+ return -1;
48
+ case 'ArrowRight':
49
+ return 1;
50
+ case 'ArrowUp':
51
+ return -rowSize;
52
+ case 'ArrowDown':
53
+ return rowSize;
54
+ case 'Home':
55
+ return Number.NEGATIVE_INFINITY;
56
+ case 'End':
57
+ return Number.POSITIVE_INFINITY;
58
+ default:
59
+ return null;
60
+ }
61
+ }
@@ -9,4 +9,5 @@ export type {
9
9
  export { Explorer } from './adapters/inbound/Explorer';
10
10
  export { FileExplorer } from './adapters/inbound/FileExplorer';
11
11
  export { MediaExplorer } from './adapters/inbound/MediaExplorer';
12
+ export { getExplorerNextFocusId } from './application/getExplorerNextFocusId';
12
13
  export { resolveExplorerSelection } from './application/resolveExplorerSelection';
@@ -1,4 +1,9 @@
1
- import { getVisibleGridItems, type GridViewport, worldToViewport } from '@ankhorage/grid-view';
1
+ import {
2
+ getVisibleGridItems,
3
+ type GridViewport,
4
+ revealWorldRect,
5
+ worldToViewport,
6
+ } from '@ankhorage/grid-view';
2
7
  import React from 'react';
3
8
  import { ScrollView as NativeScrollView, View as NativeView } from 'react-native';
4
9
 
@@ -18,13 +23,18 @@ export function GridView({
18
23
  height,
19
24
  zoom = 1,
20
25
  overscanPixels = 160,
26
+ focusedItemId,
27
+ revealPaddingPixels = 8,
21
28
  interactionPolicy,
22
29
  onViewportChange,
30
+ onVisibleItemIdsChange,
23
31
  renderItem,
24
32
  testID,
25
33
  }: GridViewProps) {
26
34
  const [scrollX, setScrollX] = React.useState(0);
27
35
  const [scrollY, setScrollY] = React.useState(0);
36
+ const horizontalScrollRef = React.useRef<NativeScrollView>(null);
37
+ const verticalScrollRef = React.useRef<NativeScrollView>(null);
28
38
  const scale = Math.max(0.01, zoom);
29
39
  const viewport = React.useMemo<GridViewport>(
30
40
  () => ({
@@ -41,13 +51,50 @@ export function GridView({
41
51
  () => getVisibleGridItems(items, viewport, overscanPixels),
42
52
  [items, overscanPixels, viewport],
43
53
  );
54
+ const viewportVisibleIds = React.useMemo(
55
+ () => getVisibleGridItems(visibleItems, viewport).map((item) => item.id),
56
+ [viewport, visibleItems],
57
+ );
58
+ const previousVisibleIdsRef = React.useRef<readonly string[]>([]);
59
+
60
+ React.useEffect(() => {
61
+ if (!onVisibleItemIdsChange) return;
62
+ const previous = previousVisibleIdsRef.current;
63
+ if (
64
+ previous.length === viewportVisibleIds.length &&
65
+ previous.every((id, index) => id === viewportVisibleIds.at(index))
66
+ ) {
67
+ return;
68
+ }
69
+ previousVisibleIdsRef.current = viewportVisibleIds;
70
+ onVisibleItemIdsChange(viewportVisibleIds);
71
+ }, [onVisibleItemIdsChange, viewportVisibleIds]);
44
72
 
45
73
  React.useEffect(() => {
46
74
  onViewportChange?.(viewport);
47
75
  }, [onViewportChange, viewport]);
48
76
 
77
+ const viewportRef = React.useRef(viewport);
78
+ React.useEffect(() => {
79
+ viewportRef.current = viewport;
80
+ }, [viewport]);
81
+
82
+ React.useEffect(() => {
83
+ const currentViewport = viewportRef.current;
84
+ const focusedItem = items.find((item) => item.id === focusedItemId);
85
+ if (!focusedItem) return;
86
+ const revealed = revealWorldRect(currentViewport, focusedItem, revealPaddingPixels);
87
+ if (revealed.offsetX !== currentViewport.offsetX) {
88
+ horizontalScrollRef.current?.scrollTo({ x: revealed.offsetX * scale, animated: true });
89
+ }
90
+ if (revealed.offsetY !== currentViewport.offsetY) {
91
+ verticalScrollRef.current?.scrollTo({ y: revealed.offsetY * scale, animated: true });
92
+ }
93
+ }, [focusedItemId, height, items, revealPaddingPixels, scale, width]);
94
+
49
95
  return (
50
96
  <NativeScrollView
97
+ ref={horizontalScrollRef}
51
98
  horizontal
52
99
  scrollEnabled={interactionPolicy !== 'passive'}
53
100
  scrollEventThrottle={32}
@@ -57,6 +104,7 @@ export function GridView({
57
104
  onScroll={(event) => setScrollX(event.nativeEvent.contentOffset.x)}
58
105
  >
59
106
  <NativeScrollView
107
+ ref={verticalScrollRef}
60
108
  scrollEnabled={interactionPolicy !== 'passive'}
61
109
  scrollEventThrottle={32}
62
110
  showsVerticalScrollIndicator
@@ -14,6 +14,10 @@ export function TileGrid({
14
14
  gap = 12,
15
15
  zoom = 1,
16
16
  overscanPixels,
17
+ focusedItemId,
18
+ revealPaddingPixels,
19
+ onColumnsChange,
20
+ onVisibleItemIdsChange,
17
21
  renderItem,
18
22
  interactionPolicy,
19
23
  testID,
@@ -26,6 +30,10 @@ export function TileGrid({
26
30
  );
27
31
  const itemLookup = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
28
32
 
33
+ React.useEffect(() => {
34
+ if (resolvedWidth > 0) onColumnsChange?.(layout.columns);
35
+ }, [layout.columns, onColumnsChange, resolvedWidth]);
36
+
29
37
  return (
30
38
  <NativeView
31
39
  onLayout={(event) => setMeasuredWidth(event.nativeEvent.layout.width)}
@@ -37,13 +45,16 @@ export function TileGrid({
37
45
  contentHeight={layout.height}
38
46
  contentWidth={Math.max(resolvedWidth / zoom, layout.width)}
39
47
  height={height}
48
+ focusedItemId={focusedItemId}
40
49
  interactionPolicy={interactionPolicy}
41
50
  items={layout.items}
51
+ onVisibleItemIdsChange={onVisibleItemIdsChange}
42
52
  overscanPixels={overscanPixels}
43
53
  renderItem={(item) => {
44
54
  const source = itemLookup.get(item.id);
45
55
  return source ? renderItem(source) : null;
46
56
  }}
57
+ revealPaddingPixels={revealPaddingPixels}
47
58
  width={resolvedWidth}
48
59
  zoom={zoom}
49
60
  />
@@ -12,7 +12,12 @@ export interface GridViewProps extends ZoraBaseProps {
12
12
  height: number;
13
13
  zoom?: number;
14
14
  overscanPixels?: number;
15
+ /** Stable item identity that must be scrolled into view without duplicating viewport geometry. */
16
+ focusedItemId?: string;
17
+ /** Pixel inset retained around a revealed focused item. */
18
+ revealPaddingPixels?: number;
15
19
  onViewportChange?: (viewport: GridViewport) => void;
20
+ onVisibleItemIdsChange?: (ids: readonly string[]) => void;
16
21
  renderItem: (item: GridRectItem) => React.ReactNode;
17
22
  }
18
23
 
@@ -30,5 +35,10 @@ export interface TileGridProps extends ZoraBaseProps {
30
35
  gap?: number;
31
36
  zoom?: number;
32
37
  overscanPixels?: number;
38
+ focusedItemId?: string;
39
+ revealPaddingPixels?: number;
40
+ /** Reports the same measured column count used to position tiles. */
41
+ onColumnsChange?: (columns: number) => void;
42
+ onVisibleItemIdsChange?: (ids: readonly string[]) => void;
33
43
  renderItem: (item: TileGridItem) => React.ReactNode;
34
44
  }
@@ -36,6 +36,42 @@ function getVisibleGridItems(items, viewport, overscanPixels = 0) {
36
36
  const bounds = getVisibleWorldBounds(viewport, overscanPixels);
37
37
  return items.filter((item) => item.x <= bounds.x + bounds.width && item.x + item.width >= bounds.x && item.y <= bounds.y + bounds.height && item.y + item.height >= bounds.y);
38
38
  }
39
+ // node_modules/@ankhorage/grid-view/dist/features/viewport/revealWorldRect.js
40
+ function revealWorldRect(viewport, rect, paddingPixels = 0) {
41
+ validateRevealInput(viewport, rect, paddingPixels);
42
+ return {
43
+ ...viewport,
44
+ offsetX: revealAxis(viewport.offsetX, viewport.width, viewport.pixelsPerUnitX, rect.x, rect.width, paddingPixels),
45
+ offsetY: revealAxis(viewport.offsetY, viewport.height, viewport.pixelsPerUnitY, rect.y, rect.height, paddingPixels)
46
+ };
47
+ }
48
+ function revealAxis(offset, viewportPixels, pixelsPerUnit, rectStart, rectSize, paddingPixels) {
49
+ const visibleStart = offset + paddingPixels / pixelsPerUnit;
50
+ const visibleEnd = offset + (viewportPixels - paddingPixels) / pixelsPerUnit;
51
+ const rectEnd = rectStart + rectSize;
52
+ if (rectSize > visibleEnd - visibleStart || rectStart < visibleStart) {
53
+ return rectStart - paddingPixels / pixelsPerUnit;
54
+ }
55
+ if (rectEnd > visibleEnd) {
56
+ return rectEnd - (viewportPixels - paddingPixels) / pixelsPerUnit;
57
+ }
58
+ return offset;
59
+ }
60
+ function validateRevealInput(viewport, rect, paddingPixels) {
61
+ if (![
62
+ viewport.width,
63
+ viewport.height,
64
+ viewport.offsetX,
65
+ viewport.offsetY,
66
+ viewport.pixelsPerUnitX,
67
+ viewport.pixelsPerUnitY
68
+ ].every(Number.isFinite) || viewport.width <= 0 || viewport.height <= 0 || viewport.pixelsPerUnitX <= 0 || viewport.pixelsPerUnitY <= 0 || !Number.isFinite(paddingPixels) || paddingPixels < 0 || paddingPixels * 2 >= viewport.width || paddingPixels * 2 >= viewport.height) {
69
+ throw new RangeError("Viewport geometry must be finite and padding must leave visible space.");
70
+ }
71
+ if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) || rect.width < 0 || rect.height < 0) {
72
+ throw new RangeError("World rectangles must have finite nonnegative dimensions.");
73
+ }
74
+ }
39
75
  // node_modules/@ankhorage/grid-view/dist/features/viewport/worldToViewport.js
40
76
  function worldToViewport(point, viewport) {
41
77
  return {
@@ -55,13 +91,18 @@ function GridView2({
55
91
  height,
56
92
  zoom = 1,
57
93
  overscanPixels = 160,
94
+ focusedItemId,
95
+ revealPaddingPixels = 8,
58
96
  interactionPolicy,
59
97
  onViewportChange,
98
+ onVisibleItemIdsChange,
60
99
  renderItem,
61
100
  testID
62
101
  }) {
63
102
  const [scrollX, setScrollX] = React.useState(0);
64
103
  const [scrollY, setScrollY] = React.useState(0);
104
+ const horizontalScrollRef = React.useRef(null);
105
+ const verticalScrollRef = React.useRef(null);
65
106
  const scale = Math.max(0.01, zoom);
66
107
  const viewport = React.useMemo(() => ({
67
108
  width,
@@ -72,10 +113,40 @@ function GridView2({
72
113
  pixelsPerUnitY: scale
73
114
  }), [height, scale, scrollX, scrollY, width]);
74
115
  const visibleItems = React.useMemo(() => getVisibleGridItems(items, viewport, overscanPixels), [items, overscanPixels, viewport]);
116
+ const viewportVisibleIds = React.useMemo(() => getVisibleGridItems(visibleItems, viewport).map((item) => item.id), [viewport, visibleItems]);
117
+ const previousVisibleIdsRef = React.useRef([]);
118
+ React.useEffect(() => {
119
+ if (!onVisibleItemIdsChange)
120
+ return;
121
+ const previous = previousVisibleIdsRef.current;
122
+ if (previous.length === viewportVisibleIds.length && previous.every((id, index) => id === viewportVisibleIds.at(index))) {
123
+ return;
124
+ }
125
+ previousVisibleIdsRef.current = viewportVisibleIds;
126
+ onVisibleItemIdsChange(viewportVisibleIds);
127
+ }, [onVisibleItemIdsChange, viewportVisibleIds]);
75
128
  React.useEffect(() => {
76
129
  onViewportChange?.(viewport);
77
130
  }, [onViewportChange, viewport]);
131
+ const viewportRef = React.useRef(viewport);
132
+ React.useEffect(() => {
133
+ viewportRef.current = viewport;
134
+ }, [viewport]);
135
+ React.useEffect(() => {
136
+ const currentViewport = viewportRef.current;
137
+ const focusedItem = items.find((item) => item.id === focusedItemId);
138
+ if (!focusedItem)
139
+ return;
140
+ const revealed = revealWorldRect(currentViewport, focusedItem, revealPaddingPixels);
141
+ if (revealed.offsetX !== currentViewport.offsetX) {
142
+ horizontalScrollRef.current?.scrollTo({ x: revealed.offsetX * scale, animated: true });
143
+ }
144
+ if (revealed.offsetY !== currentViewport.offsetY) {
145
+ verticalScrollRef.current?.scrollTo({ y: revealed.offsetY * scale, animated: true });
146
+ }
147
+ }, [focusedItemId, height, items, revealPaddingPixels, scale, width]);
78
148
  return /* @__PURE__ */ jsx(import_react_native.ScrollView, {
149
+ ref: horizontalScrollRef,
79
150
  horizontal: true,
80
151
  scrollEnabled: interactionPolicy !== "passive",
81
152
  scrollEventThrottle: 32,
@@ -84,6 +155,7 @@ function GridView2({
84
155
  testID,
85
156
  onScroll: (event) => setScrollX(event.nativeEvent.contentOffset.x),
86
157
  children: /* @__PURE__ */ jsx(import_react_native.ScrollView, {
158
+ ref: verticalScrollRef,
87
159
  scrollEnabled: interactionPolicy !== "passive",
88
160
  scrollEventThrottle: 32,
89
161
  showsVerticalScrollIndicator: true,
@@ -148,6 +220,10 @@ function TileGrid2({
148
220
  gap = 12,
149
221
  zoom = 1,
150
222
  overscanPixels,
223
+ focusedItemId,
224
+ revealPaddingPixels,
225
+ onColumnsChange,
226
+ onVisibleItemIdsChange,
151
227
  renderItem,
152
228
  interactionPolicy,
153
229
  testID
@@ -156,6 +232,10 @@ function TileGrid2({
156
232
  const resolvedWidth = width ?? measuredWidth;
157
233
  const layout = React2.useMemo(() => layoutTileGrid(items, resolvedWidth, tileSize, gap, zoom), [items, resolvedWidth, tileSize, gap, zoom]);
158
234
  const itemLookup = React2.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
235
+ React2.useEffect(() => {
236
+ if (resolvedWidth > 0)
237
+ onColumnsChange?.(layout.columns);
238
+ }, [layout.columns, onColumnsChange, resolvedWidth]);
159
239
  return /* @__PURE__ */ jsx2(import_react_native2.View, {
160
240
  onLayout: (event) => setMeasuredWidth(event.nativeEvent.layout.width),
161
241
  style: { width, height, overflow: "hidden" },
@@ -164,13 +244,16 @@ function TileGrid2({
164
244
  contentHeight: layout.height,
165
245
  contentWidth: Math.max(resolvedWidth / zoom, layout.width),
166
246
  height,
247
+ focusedItemId,
167
248
  interactionPolicy,
168
249
  items: layout.items,
250
+ onVisibleItemIdsChange,
169
251
  overscanPixels,
170
252
  renderItem: (item) => {
171
253
  const source = itemLookup.get(item.id);
172
254
  return source ? renderItem(source) : null;
173
255
  },
256
+ revealPaddingPixels,
174
257
  width: resolvedWidth,
175
258
  zoom
176
259
  }) : null