panelui-native 0.86.1 → 0.88.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/README.md +4 -2
  2. package/lib/module/components/animated-badge/index.js +451 -0
  3. package/lib/module/components/animated-badge/index.js.map +1 -0
  4. package/lib/module/components/bottom-sheet/index.js +17 -6
  5. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  6. package/lib/module/components/bubble-chart/index.js +1782 -0
  7. package/lib/module/components/bubble-chart/index.js.map +1 -0
  8. package/lib/module/components/feedback-dialog/index.js +474 -0
  9. package/lib/module/components/feedback-dialog/index.js.map +1 -0
  10. package/lib/module/components/pyramid-chart/index.js +1143 -0
  11. package/lib/module/components/pyramid-chart/index.js.map +1 -0
  12. package/lib/module/components/scroll-blur/index.js +363 -0
  13. package/lib/module/components/scroll-blur/index.js.map +1 -0
  14. package/lib/module/components/scroll-canvas/index.js +2 -0
  15. package/lib/module/components/scroll-canvas/index.js.map +1 -1
  16. package/lib/module/components/scroll-text/index.js +3 -0
  17. package/lib/module/components/scroll-text/index.js.map +1 -1
  18. package/lib/module/components/search-bar/index.js +753 -25
  19. package/lib/module/components/search-bar/index.js.map +1 -1
  20. package/lib/module/hooks/use-keyboard-avoidance.js +21 -1
  21. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  22. package/lib/module/hooks/use-reveal-progress.js +21 -1
  23. package/lib/module/hooks/use-reveal-progress.js.map +1 -1
  24. package/lib/module/index.js +5 -0
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/utils/chart.js +24 -0
  27. package/lib/module/utils/chart.js.map +1 -1
  28. package/lib/typescript/src/components/animated-badge/index.d.ts +224 -0
  29. package/lib/typescript/src/components/animated-badge/index.d.ts.map +1 -0
  30. package/lib/typescript/src/components/bottom-sheet/index.d.ts +1 -15
  31. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  32. package/lib/typescript/src/components/bubble-chart/index.d.ts +481 -0
  33. package/lib/typescript/src/components/bubble-chart/index.d.ts.map +1 -0
  34. package/lib/typescript/src/components/feedback-dialog/index.d.ts +156 -0
  35. package/lib/typescript/src/components/feedback-dialog/index.d.ts.map +1 -0
  36. package/lib/typescript/src/components/pyramid-chart/index.d.ts +328 -0
  37. package/lib/typescript/src/components/pyramid-chart/index.d.ts.map +1 -0
  38. package/lib/typescript/src/components/scroll-blur/index.d.ts +113 -0
  39. package/lib/typescript/src/components/scroll-blur/index.d.ts.map +1 -0
  40. package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -1
  41. package/lib/typescript/src/components/search-bar/index.d.ts +350 -6
  42. package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
  43. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  44. package/lib/typescript/src/hooks/use-reveal-progress.d.ts +6 -27
  45. package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -1
  46. package/lib/typescript/src/index.d.ts +6 -1
  47. package/lib/typescript/src/index.d.ts.map +1 -1
  48. package/lib/typescript/src/utils/chart.d.ts +14 -0
  49. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  50. package/package.json +1 -1
  51. package/src/components/animated-badge/index.tsx +513 -0
  52. package/src/components/bottom-sheet/index.tsx +23 -8
  53. package/src/components/bubble-chart/index.tsx +2145 -0
  54. package/src/components/feedback-dialog/index.tsx +557 -0
  55. package/src/components/pyramid-chart/index.tsx +1360 -0
  56. package/src/components/scroll-blur/index.tsx +466 -0
  57. package/src/components/scroll-canvas/index.tsx +2 -1
  58. package/src/components/scroll-text/index.tsx +3 -3
  59. package/src/components/search-bar/index.tsx +933 -45
  60. package/src/hooks/use-keyboard-avoidance.ts +24 -1
  61. package/src/hooks/use-reveal-progress.ts +30 -1
  62. package/src/index.ts +72 -1
  63. package/src/utils/chart.ts +27 -0
@@ -0,0 +1,466 @@
1
+ /**
2
+ * ScrollBlur — blurs the edges of a scroll container.
3
+ *
4
+ * Content that runs past a boundary reads better receding than being cut off,
5
+ * and a receding edge doubles as an affordance: an edge that is going soft is
6
+ * an edge with more content behind it.
7
+ *
8
+ * A blur says that where a fade cannot: a fade takes the content towards the
9
+ * colour behind the scroller, so it only works where that colour is known and
10
+ * flat. A blur takes it out of focus instead, which is true over a photograph,
11
+ * a gradient, or a list of coloured cards. It is also what belongs under
12
+ * something laid *over* the scroller — a button, a header, a search field —
13
+ * because the content passing beneath stays visible as shape and colour while
14
+ * losing the detail that would compete with the thing on top.
15
+ *
16
+ * ```tsx
17
+ * <ScrollBlur>
18
+ * <ScrollView>…</ScrollView>
19
+ * </ScrollBlur>
20
+ * ```
21
+ *
22
+ * ## The ramp is a stack and a wash, because neither alone is smooth
23
+ *
24
+ * A blur that goes from nothing to full across a band needs a per-pixel blur
25
+ * radius, and there is no such thing on either platform — a blur view has one
26
+ * strength for its whole rectangle.
27
+ *
28
+ * So the ramp is built out of several of them: each layer covers a shorter
29
+ * span than the last, measured from the edge, and each blurs what the layer
30
+ * under it has already blurred. The spans are spaced on a curve rather than
31
+ * evenly, which puts most of the layers in the outer third where the blur is
32
+ * changing fastest and the steps would otherwise be widest.
33
+ *
34
+ * That alone is not enough. Every layer has a hard edge, and a stack of hard
35
+ * edges is a stack of visible lines however many there are. So a gradient of
36
+ * `color` is washed over the top — opaque at the outer edge, clear at the
37
+ * inner one. It hides the seams, and it is what makes the band read as one
38
+ * material rather than as a pile of rectangles: the content goes soft and
39
+ * fades into the surface at the same time, which is what the eye expects an
40
+ * edge to do.
41
+ *
42
+ * That wash is why `color` matters even when the blur is drawn. Give it the
43
+ * surface the scrollable actually sits on — a sheet, a card, the page — or the
44
+ * band fades towards a colour that is not there.
45
+ *
46
+ * ## Where it cannot blur, it fades
47
+ *
48
+ * A real blur needs a native view. `expo-blur` is optional, and Reduce
49
+ * Transparency is a preference that outranks the design, so both cases fall
50
+ * back to a gradient towards `color` — the same thing `ScrollFade` draws. A
51
+ * blur you cannot draw is better shown as a fade than as a hard edge, and far
52
+ * better than as a crash.
53
+ *
54
+ * Because of that fallback, pass `color` whenever the scroller does not sit on
55
+ * the theme background. It is unused in the blurred case and the whole effect
56
+ * in the other one.
57
+ *
58
+ * Scroll position, content size and viewport size are held in shared values
59
+ * and read on the UI thread, so scrolling never re-renders React.
60
+ */
61
+ import {
62
+ Children,
63
+ isValidElement,
64
+ useMemo,
65
+ type ComponentType,
66
+ type ReactElement,
67
+ type ReactNode,
68
+ } from 'react';
69
+ import {
70
+ StyleSheet,
71
+ View,
72
+ type LayoutChangeEvent,
73
+ type ViewProps,
74
+ } from 'react-native';
75
+ import { LinearGradient } from 'expo-linear-gradient';
76
+ import Animated, {
77
+ useAnimatedScrollHandler,
78
+ useAnimatedStyle,
79
+ useComposedEventHandler,
80
+ useDerivedValue,
81
+ useSharedValue,
82
+ type AnimatedScrollViewProps,
83
+ type DerivedValue,
84
+ } from 'react-native-reanimated';
85
+ import { useCSSVariable } from 'uniwind';
86
+ import { hasBlur, useReduceTransparency } from '../../primitives/scrim';
87
+ import { useThemeMode } from '../../theme/use-theme';
88
+
89
+ /**
90
+ * `expo-blur`'s BlurView, or null when it is not installed. Resolved once at
91
+ * module load — the require is cheap and caching it avoids a try/catch on
92
+ * every render.
93
+ */
94
+ const BlurView: ComponentType<{
95
+ intensity?: number;
96
+ tint?: ScrollBlurTint;
97
+ style?: unknown;
98
+ }> | null = (() => {
99
+ try {
100
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
101
+ const mod = require('expo-blur');
102
+ return (mod?.BlurView as ComponentType<{ intensity?: number }>) ?? null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ })();
107
+
108
+ /** Past this many pixels from an edge, that edge's band is fully drawn. */
109
+ const DEFAULT_FADE_IN_DISTANCE = 24;
110
+
111
+ /** Which way the material tints. `default` follows the app's theme. */
112
+ export type ScrollBlurTint = 'light' | 'dark' | 'default';
113
+
114
+ export interface ScrollBlurProps extends ViewProps {
115
+ className?: string;
116
+ /** Depth of the blurred band in pixels. */
117
+ size?: number;
118
+ /** Which edges blur. */
119
+ edges?: 'both' | 'start' | 'end' | 'none';
120
+ /**
121
+ * Scroll axis. Inferred from the child's `horizontal` prop when omitted —
122
+ * pass it explicitly for children that scroll horizontally without that prop
123
+ * (a `FlatList` with `horizontal` set through `contentContainerStyle`, say).
124
+ */
125
+ orientation?: 'horizontal' | 'vertical';
126
+ /**
127
+ * How many blur views make up the ramp. More is smoother and costs more; the
128
+ * band shows visible steps below four, and past eight nobody can tell.
129
+ */
130
+ layers?: number;
131
+ /** Blur strength at the very edge, 0–100. The layers share it between them. */
132
+ intensity?: number;
133
+ /**
134
+ * Which way the material tints. Defaults to the app's theme rather than the
135
+ * phone's, so an app running dark on a light phone does not blur light.
136
+ */
137
+ material?: ScrollBlurTint;
138
+ /**
139
+ * The colour the band fades towards — washed over the blur, and the whole
140
+ * effect where there is no blur to draw.
141
+ *
142
+ * Defaults to the theme's background. Pass the surface the scrollable
143
+ * actually sits on, or the band fades towards a colour that is not there.
144
+ */
145
+ color?: string;
146
+ /**
147
+ * How opaque that wash gets at the outer edge, 0 to 1. Lower it to let more
148
+ * of the content show through the far end of the band; `0` leaves the blur
149
+ * bare, along with the seams between its layers.
150
+ */
151
+ tint?: number;
152
+ /** Distance in pixels over which an edge comes in from clear to full. */
153
+ fadeInDistance?: number;
154
+ /** Set false to render the child with no bands at all. */
155
+ enabled?: boolean;
156
+ children?: ReactNode;
157
+ }
158
+
159
+ interface ScrollableProps {
160
+ horizontal?: boolean;
161
+ onScroll?: AnimatedScrollViewProps['onScroll'];
162
+ onLayout?: (event: LayoutChangeEvent) => void;
163
+ onContentSizeChange?: (width: number, height: number) => void;
164
+ scrollEventThrottle?: number;
165
+ }
166
+
167
+ export function ScrollBlur({
168
+ className,
169
+ size = 80,
170
+ edges = 'both',
171
+ orientation,
172
+ layers = 8,
173
+ intensity = 56,
174
+ material = 'default',
175
+ tint = 0.95,
176
+ color,
177
+ fadeInDistance = DEFAULT_FADE_IN_DISTANCE,
178
+ enabled = true,
179
+ children,
180
+ ...props
181
+ }: ScrollBlurProps) {
182
+ const offset = useSharedValue(0);
183
+ const contentLength = useSharedValue(0);
184
+ const viewportLength = useSharedValue(0);
185
+
186
+ const reduceTransparency = useReduceTransparency();
187
+ const { mode } = useThemeMode();
188
+ /*
189
+ * Not knowing yet counts as "do not blur": the answer is asynchronous and
190
+ * arriving a frame late costs nothing, while a blur flashed at somebody who
191
+ * switched Reduce Transparency on is the whole thing that setting is for.
192
+ */
193
+ const blurring = hasBlur && BlurView !== null && reduceTransparency === false;
194
+ const materialTint: ScrollBlurTint = material === 'default' ? mode : material;
195
+
196
+ const themeBackground = useCSSVariable('--color-background');
197
+ const fadeColor =
198
+ color ?? (typeof themeBackground === 'string' ? themeBackground : '#000000');
199
+
200
+ const child = Children.only(children) as ReactElement<ScrollableProps>;
201
+ const horizontal =
202
+ orientation !== undefined
203
+ ? orientation === 'horizontal'
204
+ : isValidElement(child) && !!child.props.horizontal;
205
+
206
+ // Reanimated can only drive a scroll handler on an animated component, and
207
+ // `Animated.ScrollView` is not necessarily what was passed in — the child may
208
+ // be a FlatList, a SectionList or a custom scrollable. Keyed on the element
209
+ // *type*, not the element: rebuilding the wrapper would remount the list.
210
+ const childType = isValidElement(child)
211
+ ? (child.type as ComponentType<ScrollableProps>)
212
+ : null;
213
+ const AnimatedScrollable = useMemo(
214
+ () => (childType ? Animated.createAnimatedComponent(childType) : null),
215
+ [childType]
216
+ );
217
+
218
+ const scrollHandler = useAnimatedScrollHandler({
219
+ onScroll: (event) => {
220
+ const { contentOffset, contentSize, layoutMeasurement } = event;
221
+ offset.value = horizontal ? contentOffset.x : contentOffset.y;
222
+ contentLength.value = horizontal ? contentSize.width : contentSize.height;
223
+ viewportLength.value = horizontal
224
+ ? layoutMeasurement.width
225
+ : layoutMeasurement.height;
226
+ },
227
+ });
228
+
229
+ // A consumer `onScroll` is composed rather than dropped — but because the
230
+ // child is now an animated component it has to be an animated handler too.
231
+ const onScroll = useComposedEventHandler([
232
+ scrollHandler,
233
+ (child.props.onScroll as typeof scrollHandler | undefined) ?? null,
234
+ ]);
235
+
236
+ // Measured up front as well as on scroll, so an end edge with content behind
237
+ // it is drawn from the first frame rather than waiting for a scroll event.
238
+ const onLayout = (event: LayoutChangeEvent) => {
239
+ const { width, height } = event.nativeEvent.layout;
240
+ viewportLength.value = horizontal ? width : height;
241
+ child.props.onLayout?.(event);
242
+ };
243
+
244
+ const onContentSizeChange = (width: number, height: number) => {
245
+ contentLength.value = horizontal ? width : height;
246
+ child.props.onContentSizeChange?.(width, height);
247
+ };
248
+
249
+ const startOpacity = useDerivedValue(() =>
250
+ Math.min(offset.value / fadeInDistance, 1)
251
+ );
252
+
253
+ const endOpacity = useDerivedValue(() => {
254
+ // Nothing to blur towards when the content fits inside the viewport.
255
+ const remaining = contentLength.value - viewportLength.value - offset.value;
256
+ return Math.min(Math.max(remaining, 0) / fadeInDistance, 1);
257
+ });
258
+
259
+ const scrollable =
260
+ AnimatedScrollable && isValidElement(child) ? (
261
+ <AnimatedScrollable
262
+ {...child.props}
263
+ onScroll={onScroll}
264
+ onLayout={onLayout}
265
+ onContentSizeChange={onContentSizeChange}
266
+ scrollEventThrottle={child.props.scrollEventThrottle ?? 16}
267
+ />
268
+ ) : (
269
+ child
270
+ );
271
+
272
+ if (!enabled || edges === 'none') {
273
+ return (
274
+ <View {...props} className={className}>
275
+ {scrollable}
276
+ </View>
277
+ );
278
+ }
279
+
280
+ const edgeProps = {
281
+ size,
282
+ horizontal,
283
+ layers: Math.max(1, Math.round(layers)),
284
+ intensity,
285
+ material: materialTint,
286
+ tint: Math.max(0, Math.min(1, tint)),
287
+ blurring,
288
+ color: fadeColor,
289
+ };
290
+
291
+ return (
292
+ <View {...props} className={className}>
293
+ {scrollable}
294
+
295
+ {edges !== 'end' ? (
296
+ <Edge {...edgeProps} opacity={startOpacity} edge="start" />
297
+ ) : null}
298
+ {edges !== 'start' ? (
299
+ <Edge {...edgeProps} opacity={endOpacity} edge="end" />
300
+ ) : null}
301
+ </View>
302
+ );
303
+ }
304
+
305
+ ScrollBlur.displayName = 'ScrollBlur';
306
+
307
+ /** One edge's band: the stack of blur views, and the wash that joins them up. */
308
+ function Edge({
309
+ size,
310
+ horizontal,
311
+ layers,
312
+ intensity,
313
+ material,
314
+ tint,
315
+ blurring,
316
+ color,
317
+ opacity,
318
+ edge,
319
+ }: {
320
+ size: number;
321
+ horizontal: boolean;
322
+ layers: number;
323
+ intensity: number;
324
+ material: ScrollBlurTint;
325
+ tint: number;
326
+ blurring: boolean;
327
+ color: string;
328
+ opacity: DerivedValue<number>;
329
+ edge: 'start' | 'end';
330
+ }) {
331
+ const isStart = edge === 'start';
332
+
333
+ const position = horizontal
334
+ ? { top: 0, bottom: 0, width: size, ...(isStart ? { left: 0 } : { right: 0 }) }
335
+ : { left: 0, right: 0, height: size, ...(isStart ? { top: 0 } : { bottom: 0 }) };
336
+
337
+ const style = useAnimatedStyle(() => ({ opacity: opacity.value }));
338
+
339
+ /** How opaque the wash gets at the outer edge. Without a blur it is all of it. */
340
+ const full = blurring ? tint : 1;
341
+
342
+ const steps = useMemo(
343
+ () =>
344
+ Array.from({ length: layers }, (_unused, index) => {
345
+ /*
346
+ * Spans on a curve rather than evenly spaced. The blur's strength grows
347
+ * fastest near the edge, so that is where the steps between layers
348
+ * would be widest — squaring the fraction crowds most of the layers
349
+ * into the outer third and spreads the rest thin across the inner
350
+ * two, which is where nothing much is happening anyway.
351
+ */
352
+ const fraction = (layers - index) / layers;
353
+ return {
354
+ key: index,
355
+ // Cubed rather than squared: with eight layers a square curve still
356
+ // leaves the outermost two nearly the same width, and two layers of
357
+ // the same width are one layer at twice the cost.
358
+ span: size * fraction * fraction * fraction,
359
+ /*
360
+ * The widest layer is the faintest, so the band starts from nothing
361
+ * instead of stepping up at its inner boundary. Squared, so the
362
+ * fade-in is gentle where the layers are widest and the eye has the
363
+ * most room to notice one arriving.
364
+ */
365
+ opacity: ((index + 1) / layers) ** 2,
366
+ };
367
+ }),
368
+ [layers, size]
369
+ );
370
+
371
+ const along = (span: number) =>
372
+ horizontal
373
+ ? { top: 0, bottom: 0, width: span, ...(isStart ? { left: 0 } : { right: 0 }) }
374
+ : { left: 0, right: 0, height: span, ...(isStart ? { top: 0 } : { bottom: 0 }) };
375
+
376
+ return (
377
+ <Animated.View
378
+ pointerEvents="none"
379
+ style={[{ position: 'absolute', overflow: 'hidden' }, position, style]}
380
+ >
381
+ {blurring && BlurView
382
+ ? steps.map((step) => (
383
+ <View
384
+ key={step.key}
385
+ style={[{ position: 'absolute', opacity: step.opacity }, along(step.span)]}
386
+ >
387
+ <BlurView
388
+ intensity={intensity / layers}
389
+ tint={material}
390
+ style={StyleSheet.absoluteFill}
391
+ />
392
+ </View>
393
+ ))
394
+ : null}
395
+
396
+ {/*
397
+ The wash. Over the blur it hides the seam every layer's hard edge
398
+ leaves and carries the content into the surface; without a blur to draw
399
+ it is the whole band, which is the fade this degrades to.
400
+ */}
401
+ {tint > 0 || !blurring ? (
402
+ /*
403
+ * Three stops, not two. A straight line from opaque to clear spends
404
+ * half its length at alphas high enough to read as a flat panel, then
405
+ * gives out all at once — the band gets an edge of its own, which is
406
+ * the thing it exists to remove. The middle stop at just over a third
407
+ * of the full alpha, placed past the halfway point, bends it into the
408
+ * long tail an edge is supposed to have.
409
+ */
410
+ <LinearGradient
411
+ colors={
412
+ isStart
413
+ ? [
414
+ withAlpha(color, full),
415
+ withAlpha(color, full * 0.36),
416
+ withAlpha(color, 0),
417
+ ]
418
+ : [
419
+ withAlpha(color, 0),
420
+ withAlpha(color, full * 0.36),
421
+ withAlpha(color, full),
422
+ ]
423
+ }
424
+ locations={isStart ? [0, 0.55, 1] : [0, 0.45, 1]}
425
+ start={{ x: 0, y: 0 }}
426
+ end={horizontal ? { x: 1, y: 0 } : { x: 0, y: 1 }}
427
+ style={StyleSheet.absoluteFill}
428
+ />
429
+ ) : null}
430
+ </Animated.View>
431
+ );
432
+ }
433
+
434
+ /**
435
+ * Gradients need a transparent stop of the *same* colour — `transparent` is
436
+ * black at zero alpha on Android, which shows as a grey smear.
437
+ *
438
+ * Written out here rather than shared with `ScrollFade`, because each of these
439
+ * components is also copied into a project on its own and a helper reached for
440
+ * across two of them would arrive as a file nobody asked for.
441
+ */
442
+ function withAlpha(color: string, alpha: number): string {
443
+ if (color.startsWith('#')) {
444
+ const hex = color.slice(1);
445
+ const full =
446
+ hex.length === 3
447
+ ? hex
448
+ .split('')
449
+ .map((c) => c + c)
450
+ .join('')
451
+ : hex.slice(0, 6);
452
+ const r = parseInt(full.slice(0, 2), 16);
453
+ const g = parseInt(full.slice(2, 4), 16);
454
+ const b = parseInt(full.slice(4, 6), 16);
455
+ if (Number.isNaN(r + g + b)) return color;
456
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
457
+ }
458
+
459
+ const channels = color.match(/rgba?\(([^)]+)\)/)?.[1];
460
+ if (channels) {
461
+ const [r, g, b] = channels.split(',').map((part) => part.trim());
462
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
463
+ }
464
+
465
+ return color;
466
+ }
@@ -101,7 +101,7 @@ export function ScrollCanvas({
101
101
  const reducedMotion = useReducedMotion();
102
102
  const active = enabled && !reducedMotion;
103
103
 
104
- const { ref, progress } = useRevealProgress({
104
+ const { ref, onLayout, progress } = useRevealProgress({
105
105
  start,
106
106
  end,
107
107
  progress: external,
@@ -140,6 +140,7 @@ export function ScrollCanvas({
140
140
  <View
141
141
  {...props}
142
142
  ref={ref}
143
+ onLayout={onLayout}
143
144
  style={[{ aspectRatio }, style]}
144
145
  className={cn('w-full overflow-hidden rounded-2xl bg-muted', className)}
145
146
  >
@@ -106,7 +106,7 @@ export function ScrollText({
106
106
  const reducedMotion = useReducedMotion();
107
107
  const active = enabled && !reducedMotion;
108
108
 
109
- const { ref, progress } = useRevealProgress({
109
+ const { ref, onLayout, progress } = useRevealProgress({
110
110
  start,
111
111
  end,
112
112
  progress: external,
@@ -133,7 +133,7 @@ export function ScrollText({
133
133
 
134
134
  if (asRow) {
135
135
  return (
136
- <View ref={ref} className="flex-row flex-wrap items-baseline">
136
+ <View ref={ref} onLayout={onLayout} className="flex-row flex-wrap items-baseline">
137
137
  {tokens.map((token, index) => (
138
138
  <RisingToken
139
139
  key={`${index}-${token}`}
@@ -151,7 +151,7 @@ export function ScrollText({
151
151
  }
152
152
 
153
153
  return (
154
- <View ref={ref}>
154
+ <View ref={ref} onLayout={onLayout}>
155
155
  <Text {...textProps} className={className}>
156
156
  {tokens.map((token, index) => (
157
157
  <InkedToken