panelui-native 0.94.0 → 0.95.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.
package/README.md CHANGED
@@ -283,6 +283,7 @@ memory pressure the OS can end the process with nothing in the terminal to expla
283
283
  | `ScatterChart` | Two quantities against each other, to show how they relate |
284
284
  | `ScrollCanvas` | Image frame whose contents move as you scroll |
285
285
  | `ScrollFade` | Fades the edges of a scroll container |
286
+ | `ScrollHeader` | A screen title that hands over to a compact bar as the page scrolls |
286
287
  | `ScrollText` | Text that resolves word by word as you scroll |
287
288
  | `SearchBar` | Search field with a clear button, a Cancel button and a panel of results |
288
289
  | `SectionRail` | Floating section navigator for a long screen |
@@ -0,0 +1,650 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ScrollHeader — a screen header that hands over to a compact bar as the page
5
+ * scrolls.
6
+ *
7
+ * A screen with a title has two states and every app draws both: the title
8
+ * large, at rest, with room around it, and the title small, pinned, once you
9
+ * are reading. Hand-rolling the change between them is where screens stop
10
+ * matching each other — one snaps, one crossfades at a different point, one
11
+ * forgets the bar is over a safe area — so the two states and the transit
12
+ * between them are one component here.
13
+ *
14
+ * ```tsx
15
+ * <ScrollHeader className="flex-1">
16
+ * <ScrollHeader.Bar>
17
+ * <ScrollHeader.Title>Library</ScrollHeader.Title>
18
+ * <ScrollHeader.Actions>
19
+ * <Button variant="ghost" size="icon"><SearchIcon size={18} /></Button>
20
+ * </ScrollHeader.Actions>
21
+ * </ScrollHeader.Bar>
22
+ * <ScrollHeader.Large>
23
+ * <ScrollHeader.Title>Library</ScrollHeader.Title>
24
+ * <ScrollHeader.Description>128 components</ScrollHeader.Description>
25
+ * </ScrollHeader.Large>
26
+ * <ScrollView>{rows}</ScrollView>
27
+ * </ScrollHeader>
28
+ * ```
29
+ *
30
+ * ## The band collapses; nothing inside it is animated up
31
+ *
32
+ * The header is one absolutely positioned band over the scroller, and the only
33
+ * thing driven by the scroll is its height: `bar + max(0, large - offset)`.
34
+ * The bar is anchored to its top and the large block to its bottom, so the
35
+ * band shrinking is what carries the large block up and behind the bar, and
36
+ * `overflow-hidden` is what cuts it off there.
37
+ *
38
+ * Driving the height rather than a `translateY` is what makes the rest fall
39
+ * out for free. Over-scrolling makes `large - offset` larger than `large`, so
40
+ * the band grows and a cover filling it stretches by being laid out bigger —
41
+ * no scale transform, so a photograph stretches without going soft. And the
42
+ * band's height is the inset the content needs, so there is one number rather
43
+ * than two that have to agree.
44
+ *
45
+ * ## The two titles cross-fade; neither one morphs
46
+ *
47
+ * The large title and the bar title are separate elements that fade past each
48
+ * other. A single title scaled and translated between the two positions tracks
49
+ * beautifully until the text is long enough to truncate, at which point it is
50
+ * animating between two different strings.
51
+ *
52
+ * Both are therefore in the tree at once, which is a problem for a screen
53
+ * reader — one of them is invisible and would still be read. So the crossing
54
+ * point is also published to React as `collapsed`, and whichever title is not
55
+ * being shown is hidden from accessibility. That is one re-render per crossing
56
+ * and no more: everything that runs per frame stays in shared values.
57
+ *
58
+ * ## What it needs
59
+ *
60
+ * A height to fill, and exactly one scrollable child. The child is cloned with
61
+ * the scroll handler and the content inset composed onto it, the same way
62
+ * `ScrollFade` wraps one, so a `ScrollView`, a `FlatList` or a `SectionList`
63
+ * all work unchanged.
64
+ */
65
+ import { Children, createContext, forwardRef, isValidElement, useCallback, useContext, useMemo, useState } from 'react';
66
+ import { Image, Platform, StyleSheet, View } from 'react-native';
67
+ import { LinearGradient } from 'expo-linear-gradient';
68
+ import Animated, { interpolate, runOnJS, scrollTo, useAnimatedReaction, useAnimatedRef, useAnimatedScrollHandler, useAnimatedStyle, useComposedEventHandler, useDerivedValue, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
69
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
70
+ import { tv } from 'tailwind-variants';
71
+ import { useCSSVariable } from 'uniwind';
72
+ import { Text, textChildren } from "../../primitives/text.js";
73
+ import { cn } from "../../utils/cn.js";
74
+ import { BAR_TITLE_ARRIVE, HANDOVER_DURATION, LARGE_EXIT, SURFACE_ARRIVE, bandHeight, collapseProgress, contentInset, hasSpan, isCrossing, snapTarget } from "./scroll-header-math.js";
75
+
76
+ /**
77
+ * Height of the pinned bar, in points, before the device's top inset is added.
78
+ * The platform navigation bars are 44 and 56; 48 is the target-size floor the
79
+ * rest of the library holds compact controls to, and it sits between them.
80
+ */
81
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
82
+ const BAR_HEIGHT = 48;
83
+
84
+ /**
85
+ * The scrollables Reanimated already animates. Its animated components are
86
+ * ordinary function components carrying the *inner* component's name, so there
87
+ * is nothing on one to test — but these two are module-level constants, and
88
+ * identity is exact.
89
+ */
90
+ const ANIMATED_SCROLLABLES = new Set([Animated.ScrollView, Animated.FlatList]);
91
+ const scrollHeaderVariants = tv({
92
+ slots: {
93
+ root: 'flex-1',
94
+ band: 'absolute inset-x-0 top-0 overflow-hidden',
95
+ cover: 'absolute inset-0',
96
+ bar: 'absolute inset-x-0 top-0 flex-row items-center gap-3 px-4',
97
+ barSurface: 'absolute inset-0',
98
+ large: 'absolute inset-x-0 bottom-0 gap-1 px-4 pb-3',
99
+ actions: 'ml-auto flex-row items-center gap-1'
100
+ },
101
+ variants: {
102
+ /** What the bar is drawn on once it has taken over. */
103
+ surface: {
104
+ plain: {
105
+ barSurface: 'bg-background'
106
+ },
107
+ muted: {
108
+ barSurface: 'bg-card'
109
+ },
110
+ none: {
111
+ barSurface: 'bg-transparent'
112
+ }
113
+ },
114
+ divider: {
115
+ true: {
116
+ barSurface: 'border-b border-border'
117
+ },
118
+ false: {}
119
+ }
120
+ },
121
+ defaultVariants: {
122
+ surface: 'plain',
123
+ divider: true
124
+ }
125
+ });
126
+
127
+ /** What the bar is drawn on once the large block has gone. */
128
+
129
+ /** Which half of the header a part is standing in. */
130
+
131
+ const ScrollHeaderContext = /*#__PURE__*/createContext(undefined);
132
+ function useScrollHeader(component) {
133
+ const context = useContext(ScrollHeaderContext);
134
+ if (!context) throw new Error(`${component} must be used within a <ScrollHeader>`);
135
+ return context;
136
+ }
137
+
138
+ /**
139
+ * Which half a part is in. `Title` reads it to know which size to be and which
140
+ * way to fade, so the same element can be written in both places.
141
+ */
142
+ const ScrollHeaderSlotContext = /*#__PURE__*/createContext('large');
143
+ /**
144
+ * The band, and the scroller it sits over. Everything that is not a part is
145
+ * treated as the scrollable child.
146
+ */
147
+ const ScrollHeaderRoot = /*#__PURE__*/forwardRef(({
148
+ className,
149
+ barHeight = BAR_HEIGHT,
150
+ threshold = 1,
151
+ snap = true,
152
+ stretch = true,
153
+ inset = true,
154
+ onCollapsedChange,
155
+ progress: externalProgress,
156
+ children,
157
+ ...props
158
+ }, ref) => {
159
+ const insets = useSafeAreaInsets();
160
+ const reducedMotion = useReducedMotion();
161
+ const insetTop = inset ? insets.top : 0;
162
+ const barBand = insetTop + barHeight;
163
+ const offset = useSharedValue(0);
164
+ const largeHeight = useSharedValue(0);
165
+ const [largeSize, setLargeSize] = useState(0);
166
+ const [collapsed, setCollapsed] = useState(false);
167
+ const scrollRef = useAnimatedRef();
168
+
169
+ // Measured into both a shared value, for the band's height, and React
170
+ // state, for the content inset — the inset is a resting number that only
171
+ // changes when the block is re-laid-out, so paying a render for it costs
172
+ // nothing per frame and keeps the scroller's props stable.
173
+ const measureLarge = useCallback(event => {
174
+ const {
175
+ height
176
+ } = event.nativeEvent.layout;
177
+ largeHeight.value = height;
178
+ setLargeSize(previous => Math.abs(previous - height) < 0.5 ? previous : height);
179
+ }, [largeHeight]);
180
+ const progress = useDerivedValue(() => {
181
+ const next = collapseProgress(offset.value, largeHeight.value, threshold);
182
+ // A header with a block hands over across that block's height, and the
183
+ // finger sets the pace. One without has no distance to interpolate over,
184
+ // so the step is timed rather than instant — the alternative is a bar
185
+ // whose surface appears between one frame and the next.
186
+ const value = hasSpan(largeHeight.value) ? next : withTiming(next, {
187
+ duration: HANDOVER_DURATION
188
+ });
189
+ if (externalProgress) externalProgress.value = value;
190
+ return value;
191
+ }, [threshold, externalProgress]);
192
+
193
+ // The one thing the transition tells React about. `collapsed` gates the
194
+ // accessibility of the two titles and is what `onCollapsedChange` reports;
195
+ // the reaction fires on the crossing, not on the frames either side of it.
196
+ const cross = useCallback(next => {
197
+ setCollapsed(next);
198
+ onCollapsedChange?.(next);
199
+ }, [onCollapsedChange]);
200
+ useAnimatedReaction(() => progress.value >= 1, (isCollapsed, was) => {
201
+ if (!isCrossing(isCollapsed, was)) return;
202
+ runOnJS(cross)(isCollapsed);
203
+ }, [cross]);
204
+ const settle = useCallback(velocity => {
205
+ 'worklet';
206
+
207
+ if (!snap) return;
208
+ const target = snapTarget(offset.value, largeHeight.value, velocity);
209
+ if (target === null) return;
210
+ scrollTo(scrollRef, 0, target, true);
211
+ }, [snap, largeHeight, offset, scrollRef]);
212
+ const {
213
+ parts,
214
+ scrollable
215
+ } = useMemo(() => splitChildren(children), [children]);
216
+ const childType = /*#__PURE__*/isValidElement(scrollable) ? scrollable.type : null;
217
+ // Keyed on the element type rather than the element: rebuilding the
218
+ // wrapper would remount the list and lose its scroll position. A child
219
+ // that is animated already is used as it stands — wrapping one twice is
220
+ // unsupported, and there is no marker on them to test for, so the two
221
+ // that exist are recognised by identity.
222
+ const AnimatedScrollable = useMemo(() => {
223
+ if (!childType) return null;
224
+ if (ANIMATED_SCROLLABLES.has(childType)) return childType;
225
+ return Animated.createAnimatedComponent(childType);
226
+ }, [childType]);
227
+ const childProps = /*#__PURE__*/isValidElement(scrollable) ? scrollable.props : {};
228
+ const childOnScroll = childProps.onScroll;
229
+
230
+ /*
231
+ * A consumer's own `onScroll` is kept, whichever of the two kinds it is,
232
+ * because silently dropping one looks like a bug in the scrolling rather
233
+ * than in the call site.
234
+ *
235
+ * `useEvent` returns an object wearing a function's type, so the two are
236
+ * told apart by what they actually are: an object is a Reanimated handler
237
+ * and composes onto ours, staying on the UI thread. A plain function
238
+ * cannot — `useComposedEventHandler` keeps only worklet handlers and drops
239
+ * anything else without a word — so it is called across the bridge
240
+ * instead, once per scroll event the child delivers. How often that is, is
241
+ * `scrollEventThrottle`'s answer rather than this component's: every frame
242
+ * at the default of 16, and as rare as the caller asks for above it.
243
+ */
244
+ const workletOnScroll = typeof childOnScroll === 'function' ? null : childOnScroll ?? null;
245
+ const plainOnScroll = typeof childOnScroll === 'function' ? childOnScroll : null;
246
+
247
+ // Rebuilt only when the callback itself changes, so the worklet below is
248
+ // not rebuilt on every render of the child.
249
+ const forwardScroll = useCallback(native => {
250
+ // The `nativeEvent` crosses whole; the synthetic wrapper around it does
251
+ // not, because the rest of one is a live object with methods on it.
252
+ plainOnScroll?.({
253
+ nativeEvent: native
254
+ });
255
+ }, [plainOnScroll]);
256
+ const forwards = plainOnScroll !== null;
257
+ const scrollHandler = useAnimatedScrollHandler({
258
+ onScroll: event => {
259
+ offset.value = event.contentOffset.y;
260
+ if (forwards) {
261
+ // Every field React Native puts on a scroll event, so a callback
262
+ // reading `velocity` for a direction, or `targetContentOffset` for
263
+ // where a fling is going, finds what it would have without the
264
+ // header. Listed rather than spread: the event carries Reanimated's
265
+ // own `eventName` too, and that is not part of the contract.
266
+ runOnJS(forwardScroll)({
267
+ contentInset: event.contentInset,
268
+ contentOffset: event.contentOffset,
269
+ contentSize: event.contentSize,
270
+ layoutMeasurement: event.layoutMeasurement,
271
+ velocity: event.velocity,
272
+ zoomScale: event.zoomScale,
273
+ targetContentOffset: event.targetContentOffset
274
+ });
275
+ }
276
+ },
277
+ onEndDrag: event => {
278
+ settle(event.velocity?.y ?? 0);
279
+ },
280
+ onMomentumEnd: () => {
281
+ settle(0);
282
+ }
283
+ }, [settle, forwards, forwardScroll]);
284
+ const onScroll = useComposedEventHandler([scrollHandler, workletOnScroll ?? null]);
285
+
286
+ /*
287
+ * The band's resting height, plus whatever top padding the child asked
288
+ * for. A style array overrides rather than adds, so composing ours after
289
+ * theirs would drop their padding and composing theirs after ours would
290
+ * drop the inset — and they cannot write the sum themselves, because the
291
+ * band's height is measured. Reading it and adding is the only
292
+ * arrangement where both survive.
293
+ */
294
+ const childContentStyle = StyleSheet.flatten(childProps.contentContainerStyle) ?? {};
295
+ const headerHeight = contentInset(barBand + largeSize, childContentStyle.paddingTop ?? childContentStyle.paddingVertical ?? childContentStyle.padding);
296
+ const bandStyle = useAnimatedStyle(() => ({
297
+ height: bandHeight(barBand, largeHeight.value, offset.value, stretch && !reducedMotion)
298
+ }), [barBand, stretch, reducedMotion]);
299
+ const metrics = useMemo(() => ({
300
+ barBand,
301
+ insetTop
302
+ }), [barBand, insetTop]);
303
+ const hasLarge = largeSize >= 1;
304
+ const context = useMemo(() => ({
305
+ progress,
306
+ offset,
307
+ largeHeight,
308
+ collapsed,
309
+ hasLarge,
310
+ measureLarge
311
+ }), [progress, offset, largeHeight, collapsed, hasLarge, measureLarge]);
312
+ const {
313
+ root,
314
+ band
315
+ } = scrollHeaderVariants();
316
+ return /*#__PURE__*/_jsx(ScrollHeaderContext.Provider, {
317
+ value: context,
318
+ children: /*#__PURE__*/_jsxs(View, {
319
+ ...props,
320
+ ref: ref,
321
+ className: root({
322
+ className
323
+ }),
324
+ children: [AnimatedScrollable && /*#__PURE__*/isValidElement(scrollable) ? /*#__PURE__*/_jsx(AnimatedScrollable, {
325
+ ...childProps,
326
+ ref: composeRefs(scrollRef, childProps.ref),
327
+ onScroll: onScroll,
328
+ scrollEventThrottle: childProps.scrollEventThrottle ?? 16
329
+ // The band is the inset. iOS would otherwise add one of its own
330
+ // on top of it, for a header it cannot see.
331
+ ,
332
+ contentInsetAdjustmentBehavior: childProps.contentInsetAdjustmentBehavior ?? 'never',
333
+ contentContainerStyle: [childProps.contentContainerStyle, {
334
+ paddingTop: headerHeight
335
+ }],
336
+ scrollIndicatorInsets: {
337
+ top: barBand,
338
+ ...childProps.scrollIndicatorInsets
339
+ }
340
+ }) : scrollable, /*#__PURE__*/_jsx(Animated.View, {
341
+ pointerEvents: "box-none",
342
+ style: [bandStyle, Platform.OS === 'android' ? styles.lift : null],
343
+ className: band(),
344
+ children: /*#__PURE__*/_jsxs(BandMetricsContext.Provider, {
345
+ value: metrics,
346
+ children: [parts.cover, parts.large, parts.bar]
347
+ })
348
+ })]
349
+ })
350
+ });
351
+ });
352
+ ScrollHeaderRoot.displayName = 'ScrollHeader';
353
+ const BandMetricsContext = /*#__PURE__*/createContext({
354
+ barBand: BAR_HEIGHT,
355
+ insetTop: 0
356
+ });
357
+ /**
358
+ * The pinned bar. Its contents never move; its surface fades in as the large
359
+ * block leaves, which is what makes the change read as one crossfade rather
360
+ * than two things happening at once.
361
+ */
362
+ const ScrollHeaderBar = /*#__PURE__*/forwardRef(({
363
+ className,
364
+ surface = 'plain',
365
+ divider = true,
366
+ children,
367
+ ...props
368
+ }, ref) => {
369
+ const {
370
+ progress
371
+ } = useScrollHeader('ScrollHeader.Bar');
372
+ const {
373
+ barBand,
374
+ insetTop
375
+ } = useContext(BandMetricsContext);
376
+ const {
377
+ bar,
378
+ barSurface
379
+ } = scrollHeaderVariants({
380
+ surface,
381
+ divider
382
+ });
383
+
384
+ // Closed by the time the block has gone, so the block is never seen through
385
+ // it — and never on top of whatever else the bar is carrying.
386
+ const surfaceStyle = useAnimatedStyle(() => ({
387
+ opacity: interpolate(progress.value, SURFACE_ARRIVE, [0, 1], 'clamp')
388
+ }));
389
+ return /*#__PURE__*/_jsxs(View, {
390
+ ...props,
391
+ ref: ref
392
+ // The bar owns only its own band. `box-none` above it lets a pull
393
+ // land on the scroller rather than on the header covering it.
394
+ ,
395
+ style: {
396
+ height: barBand,
397
+ paddingTop: insetTop
398
+ },
399
+ className: bar({
400
+ className
401
+ }),
402
+ children: [/*#__PURE__*/_jsx(Animated.View, {
403
+ pointerEvents: "none",
404
+ style: surfaceStyle,
405
+ className: barSurface()
406
+ }), /*#__PURE__*/_jsx(ScrollHeaderSlotContext.Provider, {
407
+ value: "bar",
408
+ children: textChildren(children)
409
+ })]
410
+ });
411
+ });
412
+ ScrollHeaderBar.displayName = 'ScrollHeader.Bar';
413
+ /**
414
+ * The expanded block. Its measured height is the distance the header
415
+ * collapses over, so whatever is put in it — a title, a search field, a row of
416
+ * chips — sets the scroll distance rather than a prop having to agree with it.
417
+ */
418
+ const ScrollHeaderLarge = /*#__PURE__*/forwardRef(({
419
+ className,
420
+ onLayout,
421
+ children,
422
+ ...props
423
+ }, ref) => {
424
+ const {
425
+ progress,
426
+ measureLarge,
427
+ collapsed
428
+ } = useScrollHeader('ScrollHeader.Large');
429
+ const {
430
+ large
431
+ } = scrollHeaderVariants();
432
+ const style = useAnimatedStyle(() => ({
433
+ opacity: interpolate(progress.value, LARGE_EXIT, [1, 0], 'clamp')
434
+ }));
435
+
436
+ // The measurement is this block's whole job, so it is taken first and a
437
+ // consumer's own `onLayout` runs after it rather than instead of it.
438
+ const measure = useCallback(event => {
439
+ measureLarge(event);
440
+ onLayout?.(event);
441
+ }, [measureLarge, onLayout]);
442
+ return /*#__PURE__*/_jsx(Animated.View, {
443
+ ...props,
444
+ ref: ref,
445
+ onLayout: measure
446
+ // Behind the bar and faded out by the time it gets there, so it must
447
+ // not keep taking touches that belong to the content underneath.
448
+ ,
449
+ pointerEvents: collapsed ? 'none' : 'box-none',
450
+ accessibilityElementsHidden: collapsed,
451
+ importantForAccessibility: collapsed ? 'no-hide-descendants' : 'auto',
452
+ style: style,
453
+ className: large({
454
+ className
455
+ }),
456
+ children: /*#__PURE__*/_jsx(ScrollHeaderSlotContext.Provider, {
457
+ value: "large",
458
+ children: textChildren(children)
459
+ })
460
+ });
461
+ });
462
+ ScrollHeaderLarge.displayName = 'ScrollHeader.Large';
463
+
464
+ /**
465
+ * The screen's title. Written in both halves and styled from whichever it is
466
+ * in: large and at rest in the block, compact and fading in on the bar.
467
+ */
468
+ const ScrollHeaderTitle = /*#__PURE__*/forwardRef(({
469
+ className,
470
+ ...props
471
+ }, ref) => {
472
+ const {
473
+ progress,
474
+ largeHeight,
475
+ collapsed,
476
+ hasLarge
477
+ } = useScrollHeader('ScrollHeader.Title');
478
+ const slot = useContext(ScrollHeaderSlotContext);
479
+
480
+ // A bar with no block above it is not handing over from anything: it is the
481
+ // only title the screen has, and it is wanted from the first frame. Fading it
482
+ // in with the collapse would leave that screen untitled until somebody
483
+ // scrolled it.
484
+ const style = useAnimatedStyle(() => ({
485
+ opacity: hasSpan(largeHeight.value) ? interpolate(progress.value, BAR_TITLE_ARRIVE, [0, 1], 'clamp') : 1
486
+ }));
487
+ if (slot === 'large') {
488
+ return /*#__PURE__*/_jsx(Text, {
489
+ ...props,
490
+ ref: ref,
491
+ size: "3xl",
492
+ weight: "bold",
493
+ accessibilityRole: "header",
494
+ className: cn('text-foreground', className)
495
+ });
496
+ }
497
+ return /*#__PURE__*/_jsx(Animated.View, {
498
+ style: style
499
+ // The large title is the one being read until the bar has taken over.
500
+ // Both are in the tree the whole time, and only one of them should be —
501
+ // unless there is no large title, in which case this one always is.
502
+ ,
503
+ accessibilityElementsHidden: hasLarge ? !collapsed : false,
504
+ importantForAccessibility: !hasLarge || collapsed ? 'auto' : 'no-hide-descendants',
505
+ className: "flex-1",
506
+ children: /*#__PURE__*/_jsx(Text, {
507
+ ...props,
508
+ ref: ref,
509
+ size: "base",
510
+ weight: "semibold",
511
+ numberOfLines: props.numberOfLines ?? 1,
512
+ accessibilityRole: "header",
513
+ className: cn('text-foreground', className)
514
+ })
515
+ });
516
+ });
517
+ ScrollHeaderTitle.displayName = 'ScrollHeader.Title';
518
+
519
+ /** The quiet line under the title — a count, a byline, a date. */
520
+ const ScrollHeaderDescription = /*#__PURE__*/forwardRef(({
521
+ className,
522
+ ...props
523
+ }, ref) => /*#__PURE__*/_jsx(Text, {
524
+ ...props,
525
+ ref: ref,
526
+ size: "sm",
527
+ muted: true,
528
+ className: className
529
+ }));
530
+ ScrollHeaderDescription.displayName = 'ScrollHeader.Description';
531
+ /**
532
+ * The controls at the trailing end of the bar. They stay put and stay
533
+ * reachable — only the bar's surface and title are part of the transition.
534
+ */
535
+ const ScrollHeaderActions = /*#__PURE__*/forwardRef(({
536
+ className,
537
+ children,
538
+ ...props
539
+ }, ref) => {
540
+ const {
541
+ actions
542
+ } = scrollHeaderVariants();
543
+ return /*#__PURE__*/_jsx(View, {
544
+ ...props,
545
+ ref: ref,
546
+ className: actions({
547
+ className
548
+ }),
549
+ children: textChildren(children)
550
+ });
551
+ });
552
+ ScrollHeaderActions.displayName = 'ScrollHeader.Actions';
553
+ /**
554
+ * A picture or a gradient filling the band. It has no height of its own — the
555
+ * band's height is its height, which is why it stretches on an over-scroll by
556
+ * being laid out larger rather than by being scaled up and going soft.
557
+ */
558
+ const ScrollHeaderCover = /*#__PURE__*/forwardRef(({
559
+ className,
560
+ source,
561
+ colors,
562
+ scrim = true,
563
+ children,
564
+ ...props
565
+ }, ref) => {
566
+ const {
567
+ cover
568
+ } = scrollHeaderVariants();
569
+ const seriesOne = useCSSVariable('--color-chart-1');
570
+ const seriesTwo = useCSSVariable('--color-chart-2');
571
+ const ramp = colors ?? [typeof seriesOne === 'string' ? seriesOne : '#6366f1', typeof seriesTwo === 'string' ? seriesTwo : '#8b5cf6'];
572
+ return /*#__PURE__*/_jsxs(View, {
573
+ ...props,
574
+ ref: ref,
575
+ pointerEvents: "none",
576
+ className: cover({
577
+ className
578
+ }),
579
+ children: [/*#__PURE__*/_jsx(LinearGradient, {
580
+ colors: ramp,
581
+ start: {
582
+ x: 0,
583
+ y: 0
584
+ },
585
+ end: {
586
+ x: 1,
587
+ y: 1
588
+ },
589
+ style: StyleSheet.absoluteFill
590
+ }), source ? /*#__PURE__*/_jsx(Image, {
591
+ source: source,
592
+ resizeMode: "cover",
593
+ style: StyleSheet.absoluteFill
594
+ }) : null, scrim ? /*#__PURE__*/_jsx(View, {
595
+ className: "absolute inset-0 bg-black/25"
596
+ }) : null, children]
597
+ });
598
+ });
599
+ ScrollHeaderCover.displayName = 'ScrollHeader.Cover';
600
+
601
+ /**
602
+ * Sort the children into the three band slots and the one scrollable. Sorting
603
+ * by component rather than by order is what lets the example above read
604
+ * top-down — bar, block, list — while the band still draws the cover first.
605
+ */
606
+ function splitChildren(children) {
607
+ let cover = null;
608
+ let large = null;
609
+ let bar = null;
610
+ let scrollable = null;
611
+ Children.forEach(children, child => {
612
+ if (! /*#__PURE__*/isValidElement(child)) return;
613
+ if (child.type === ScrollHeaderCover) cover = child;else if (child.type === ScrollHeaderLarge) large = child;else if (child.type === ScrollHeaderBar) bar = child;else if (!scrollable) scrollable = child;
614
+ });
615
+ return {
616
+ parts: {
617
+ cover,
618
+ large,
619
+ bar
620
+ },
621
+ scrollable
622
+ };
623
+ }
624
+
625
+ /** Point several refs at one node, skipping the ones that were not given. */
626
+ function composeRefs(...refs) {
627
+ return node => {
628
+ for (const ref of refs) {
629
+ if (typeof ref === 'function') ref(node);else if (ref && typeof ref === 'object') {
630
+ ref.current = node;
631
+ }
632
+ }
633
+ };
634
+ }
635
+ const styles = StyleSheet.create({
636
+ // Android draws by elevation before z-order, so a band with none of its own
637
+ // ends up under the scroller's own background.
638
+ lift: {
639
+ elevation: 4
640
+ }
641
+ });
642
+ export const ScrollHeader = Object.assign(ScrollHeaderRoot, {
643
+ Bar: ScrollHeaderBar,
644
+ Large: ScrollHeaderLarge,
645
+ Title: ScrollHeaderTitle,
646
+ Description: ScrollHeaderDescription,
647
+ Actions: ScrollHeaderActions,
648
+ Cover: ScrollHeaderCover
649
+ });
650
+ //# sourceMappingURL=index.js.map