panelui-native 0.71.0 → 0.73.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 (146) hide show
  1. package/README.md +2 -1
  2. package/lib/module/components/accordion/index.js +10 -4
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/area-chart/index.js +1 -0
  5. package/lib/module/components/area-chart/index.js.map +1 -1
  6. package/lib/module/components/breadcrumb/index.js +2 -1
  7. package/lib/module/components/breadcrumb/index.js.map +1 -1
  8. package/lib/module/components/button/index.js +7 -5
  9. package/lib/module/components/button/index.js.map +1 -1
  10. package/lib/module/components/carousel/index.js +1 -1
  11. package/lib/module/components/carousel/index.js.map +1 -1
  12. package/lib/module/components/chip/index.js +2 -2
  13. package/lib/module/components/chip/index.js.map +1 -1
  14. package/lib/module/components/code-block/index.js.map +1 -1
  15. package/lib/module/components/collapsible/index.js +5 -1
  16. package/lib/module/components/collapsible/index.js.map +1 -1
  17. package/lib/module/components/color-picker/index.js.map +1 -1
  18. package/lib/module/components/fab/index.js +7 -6
  19. package/lib/module/components/fab/index.js.map +1 -1
  20. package/lib/module/components/flow/index.js +1 -1
  21. package/lib/module/components/flow/index.js.map +1 -1
  22. package/lib/module/components/frame/index.js +2 -2
  23. package/lib/module/components/frame/index.js.map +1 -1
  24. package/lib/module/components/grid-item/index.js +2 -2
  25. package/lib/module/components/grid-item/index.js.map +1 -1
  26. package/lib/module/components/heatmap-chart/index.js +1 -0
  27. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  28. package/lib/module/components/hex-chart/index.js +1 -0
  29. package/lib/module/components/hex-chart/index.js.map +1 -1
  30. package/lib/module/components/input-group/index.js +7 -2
  31. package/lib/module/components/input-group/index.js.map +1 -1
  32. package/lib/module/components/item/index.js +2 -2
  33. package/lib/module/components/item/index.js.map +1 -1
  34. package/lib/module/components/line-chart/index.js +1 -0
  35. package/lib/module/components/line-chart/index.js.map +1 -1
  36. package/lib/module/components/marker/index.js +2 -2
  37. package/lib/module/components/marker/index.js.map +1 -1
  38. package/lib/module/components/marquee/index.js +9 -12
  39. package/lib/module/components/marquee/index.js.map +1 -1
  40. package/lib/module/components/marquee/marquee-math.js +39 -0
  41. package/lib/module/components/marquee/marquee-math.js.map +1 -0
  42. package/lib/module/components/menu/index.js +11 -3
  43. package/lib/module/components/menu/index.js.map +1 -1
  44. package/lib/module/components/pagination/index.js +27 -11
  45. package/lib/module/components/pagination/index.js.map +1 -1
  46. package/lib/module/components/panelside/index.js +7 -2
  47. package/lib/module/components/panelside/index.js.map +1 -1
  48. package/lib/module/components/planner/index.js +26 -5
  49. package/lib/module/components/planner/index.js.map +1 -1
  50. package/lib/module/components/plot/index.js +105 -42
  51. package/lib/module/components/plot/index.js.map +1 -1
  52. package/lib/module/components/post/index.js +4 -4
  53. package/lib/module/components/post/index.js.map +1 -1
  54. package/lib/module/components/search-bar/index.js +331 -0
  55. package/lib/module/components/search-bar/index.js.map +1 -0
  56. package/lib/module/components/table/index.js +3 -4
  57. package/lib/module/components/table/index.js.map +1 -1
  58. package/lib/module/components/timeline/index.js +288 -39
  59. package/lib/module/components/timeline/index.js.map +1 -1
  60. package/lib/module/components/toggle-button/index.js +1 -1
  61. package/lib/module/components/toggle-button/index.js.map +1 -1
  62. package/lib/module/components/tree/index.js +13 -8
  63. package/lib/module/components/tree/index.js.map +1 -1
  64. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  65. package/lib/module/index.js +1 -0
  66. package/lib/module/index.js.map +1 -1
  67. package/lib/module/utils/chart.js +50 -7
  68. package/lib/module/utils/chart.js.map +1 -1
  69. package/lib/typescript/src/components/accordion/index.d.ts +2 -2
  70. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  71. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  72. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -1
  73. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  74. package/lib/typescript/src/components/code-block/index.d.ts +3 -3
  75. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -1
  76. package/lib/typescript/src/components/collapsible/index.d.ts +2 -2
  77. package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -1
  78. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
  79. package/lib/typescript/src/components/fab/index.d.ts +4 -2
  80. package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
  81. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  82. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  83. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  85. package/lib/typescript/src/components/marquee/index.d.ts +1 -1
  86. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
  87. package/lib/typescript/src/components/marquee/marquee-math.d.ts +21 -0
  88. package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -0
  89. package/lib/typescript/src/components/menu/index.d.ts +1 -1
  90. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  91. package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
  92. package/lib/typescript/src/components/panelside/index.d.ts +1 -1
  93. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  94. package/lib/typescript/src/components/plan/index.d.ts +2 -2
  95. package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
  96. package/lib/typescript/src/components/plot/index.d.ts +59 -5
  97. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  98. package/lib/typescript/src/components/search-bar/index.d.ts +216 -0
  99. package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -0
  100. package/lib/typescript/src/components/timeline/index.d.ts +35 -2
  101. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  102. package/lib/typescript/src/components/tree/index.d.ts +3 -4
  103. package/lib/typescript/src/components/tree/index.d.ts.map +1 -1
  104. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +2 -2
  105. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  106. package/lib/typescript/src/index.d.ts +2 -1
  107. package/lib/typescript/src/index.d.ts.map +1 -1
  108. package/lib/typescript/src/utils/chart.d.ts +28 -3
  109. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  110. package/package.json +1 -1
  111. package/src/components/accordion/index.tsx +17 -7
  112. package/src/components/area-chart/index.tsx +8 -0
  113. package/src/components/breadcrumb/index.tsx +2 -1
  114. package/src/components/button/index.tsx +3 -2
  115. package/src/components/carousel/index.tsx +1 -1
  116. package/src/components/chip/index.tsx +2 -2
  117. package/src/components/code-block/index.tsx +3 -3
  118. package/src/components/collapsible/index.tsx +13 -4
  119. package/src/components/color-picker/index.tsx +3 -2
  120. package/src/components/fab/index.tsx +21 -11
  121. package/src/components/flow/index.tsx +1 -1
  122. package/src/components/frame/index.tsx +2 -2
  123. package/src/components/grid-item/index.tsx +2 -2
  124. package/src/components/heatmap-chart/index.tsx +7 -1
  125. package/src/components/hex-chart/index.tsx +7 -1
  126. package/src/components/input-group/index.tsx +7 -2
  127. package/src/components/item/index.tsx +2 -2
  128. package/src/components/line-chart/index.tsx +9 -0
  129. package/src/components/marker/index.tsx +2 -2
  130. package/src/components/marquee/index.tsx +14 -12
  131. package/src/components/marquee/marquee-math.ts +37 -0
  132. package/src/components/menu/index.tsx +11 -3
  133. package/src/components/pagination/index.tsx +26 -13
  134. package/src/components/panelside/index.tsx +7 -2
  135. package/src/components/plan/index.tsx +2 -2
  136. package/src/components/planner/index.tsx +20 -5
  137. package/src/components/plot/index.tsx +183 -39
  138. package/src/components/post/index.tsx +4 -4
  139. package/src/components/search-bar/index.tsx +436 -0
  140. package/src/components/table/index.tsx +2 -2
  141. package/src/components/timeline/index.tsx +335 -39
  142. package/src/components/toggle-button/index.tsx +1 -1
  143. package/src/components/tree/index.tsx +17 -12
  144. package/src/hooks/use-keyboard-avoidance.ts +7 -2
  145. package/src/index.ts +2 -0
  146. package/src/utils/chart.ts +66 -7
@@ -11,8 +11,18 @@
11
11
  * attribute selectors for a stylesheet to key off. `Steps` resolves its states
12
12
  * the same way, so the two read as siblings.
13
13
  *
14
- * Vertical only — a horizontal timeline gives each item roughly a fifth of a
15
- * phone's width, which is not enough for a date and a title.
14
+ * ## Two orientations
15
+ *
16
+ * Vertical is the default and the ordinary case. Horizontal lays the items out
17
+ * as columns on a rail that runs off the side of the screen and is swiped
18
+ * through, which is the arrangement a long span of time wants: a decade of
19
+ * entries read top to bottom is a page nobody reaches the end of.
20
+ *
21
+ * The objection to a horizontal timeline is that each item gets a fifth of a
22
+ * phone's width, which will not hold a date and a title. The answer is that it
23
+ * does not have to fit — the rail is wider than the screen, and a column takes
24
+ * the width its contents need. An item with nothing on it collapses to a tick,
25
+ * so a quiet stretch compresses and a busy one keeps its room.
16
26
  *
17
27
  * ```tsx
18
28
  * <Timeline variant="icon" value={2}>
@@ -34,13 +44,23 @@
34
44
  * ```
35
45
  */
36
46
  import {
47
+ Children,
37
48
  createContext,
38
49
  forwardRef,
50
+ isValidElement,
39
51
  useContext,
40
52
  useMemo,
41
53
  type ReactNode,
42
54
  } from 'react';
43
- import { View, type Text as RNText, type ViewProps } from 'react-native';
55
+ import { ScrollView, View, type Text as RNText, type ViewProps } from 'react-native';
56
+ import Animated, {
57
+ interpolate,
58
+ useAnimatedScrollHandler,
59
+ useAnimatedStyle,
60
+ useReducedMotion,
61
+ useSharedValue,
62
+ type SharedValue,
63
+ } from 'react-native-reanimated';
44
64
  import { tv } from 'tailwind-variants';
45
65
  import { useCSSVariable } from 'uniwind';
46
66
  import { IconColorProvider } from '../../icons';
@@ -49,6 +69,49 @@ import { Text, type TextProps, textChildren } from '../../primitives/text';
49
69
  export type TimelineVariant = 'dot' | 'icon' | 'numbered' | 'card' | 'compact';
50
70
  /** Semantic colour for a single event, independent of progress. */
51
71
  export type TimelineTone = 'default' | 'info' | 'success' | 'warning' | 'danger';
72
+ /** Which way the sequence runs. */
73
+ export type TimelineOrientation = 'vertical' | 'horizontal';
74
+
75
+ const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
76
+
77
+ /*
78
+ * Horizontal geometry.
79
+ *
80
+ * The rail has to land on exactly the same line in every column, and the
81
+ * simplest way to guarantee that is to make the band above it a fixed height
82
+ * rather than to position the rail against measured content. So the aside is
83
+ * `HORIZONTAL_RAIL_TOP` tall in every column, the rail is drawn once across the
84
+ * whole track at that offset, and each column's tick is pulled up by half its
85
+ * own height to sit centred on it.
86
+ */
87
+ const HORIZONTAL_RAIL_TOP = 64;
88
+ const HORIZONTAL_TICK_HEIGHT = 10;
89
+
90
+ /** A column with something to say, and one with nothing. */
91
+ const HORIZONTAL_WIDE = 268;
92
+ const HORIZONTAL_NARROW = 76;
93
+
94
+ /**
95
+ * How wide a column is: what it was given, else what its contents ask for.
96
+ *
97
+ * Read by the item to size itself and by the root to build the snap offsets, so
98
+ * it lives here rather than in either — the two disagreeing would put every
99
+ * snap point half a column out.
100
+ */
101
+ function itemWidth(props: TimelineItemProps): number {
102
+ if (typeof props.width === 'number') return props.width;
103
+
104
+ let filled = false;
105
+ Children.forEach(props.children, (child) => {
106
+ if (filled || !isValidElement(child)) return;
107
+ const displayName = (child.type as { displayName?: string })?.displayName;
108
+ if (displayName !== 'Timeline.Content') return;
109
+ const inner = (child.props as { children?: ReactNode }).children;
110
+ if (inner !== undefined && inner !== null && inner !== false) filled = true;
111
+ });
112
+
113
+ return filled ? HORIZONTAL_WIDE : HORIZONTAL_NARROW;
114
+ }
52
115
 
53
116
  /** Variants whose node is a filled disc rather than an outlined ring. */
54
117
  const SOLID_VARIANTS: TimelineVariant[] = ['dot', 'card'];
@@ -107,11 +170,29 @@ const timelineVariants = tv({
107
170
  true: { separator: 'bg-primary' },
108
171
  false: { separator: 'bg-muted' },
109
172
  },
173
+ /*
174
+ * Horizontal turns the item from a row into a column and the rail from a
175
+ * line down the side into a line across the top. The aside is a fixed
176
+ * height because that height *is* where the rail sits — see
177
+ * HORIZONTAL_RAIL_TOP — and it is left-aligned rather than right, since a
178
+ * column reads from its own left edge rather than towards a rail beside it.
179
+ */
180
+ orientation: {
181
+ vertical: {},
182
+ horizontal: {
183
+ item: 'w-auto shrink-0 flex-col gap-0 pr-6',
184
+ aside: 'w-auto items-start justify-end gap-1 pb-2 pt-0',
185
+ rail: 'w-full items-start',
186
+ body: 'w-auto flex-none pb-0 pt-5',
187
+ title: 'text-sm',
188
+ },
189
+ },
110
190
  },
111
191
  defaultVariants: {
112
192
  variant: 'dot',
113
193
  tone: 'default',
114
194
  completed: false,
195
+ orientation: 'vertical',
115
196
  },
116
197
  });
117
198
 
@@ -169,6 +250,13 @@ const TONE_ICON_VAR: Record<TimelineTone, string> = {
169
250
  interface TimelineContextValue {
170
251
  activeStep: number;
171
252
  variant: TimelineVariant;
253
+ orientation: TimelineOrientation;
254
+ /** Horizontal only: where the track has been scrolled to, on the UI thread. */
255
+ scrollX: SharedValue<number>;
256
+ /** Horizontal only: each column's left edge, indexed by its `step`. */
257
+ offsets: number[];
258
+ /** Horizontal only: false when the reader has asked for less movement. */
259
+ animate: boolean;
172
260
  }
173
261
 
174
262
  interface TimelineItemContextValue {
@@ -199,18 +287,120 @@ export interface TimelineProps extends ViewProps {
199
287
  /** Steps at or below this index render as completed. */
200
288
  value?: number;
201
289
  variant?: TimelineVariant;
290
+ /**
291
+ * Which way the sequence runs. `horizontal` lays the items out as columns on
292
+ * a rail wider than the screen, swiped through rather than scrolled down.
293
+ */
294
+ orientation?: TimelineOrientation;
295
+ /**
296
+ * Horizontal only: land a flick on a column rather than between two.
297
+ *
298
+ * On by default, because the thing being moved between is a column — stopping
299
+ * halfway shows two half-columns and no whole one.
300
+ */
301
+ snap?: boolean;
202
302
  children?: ReactNode;
203
303
  }
204
304
 
205
305
  const TimelineRoot = forwardRef<View, TimelineProps>(
206
- ({ className, value = 0, variant = 'dot', children, ...props }, ref) => {
207
- const { root } = timelineVariants({ variant });
208
- const context = useMemo(() => ({ activeStep: value, variant }), [value, variant]);
306
+ (
307
+ {
308
+ className,
309
+ value = 0,
310
+ variant = 'dot',
311
+ orientation = 'vertical',
312
+ snap = true,
313
+ children,
314
+ ...props
315
+ },
316
+ ref
317
+ ) => {
318
+ const horizontal = orientation === 'horizontal';
319
+ const { root } = timelineVariants({ variant, orientation });
320
+ const reducedMotion = useReducedMotion();
321
+ const scrollX = useSharedValue(0);
322
+ const border = useCSSVariable('--color-border');
323
+
324
+ /*
325
+ * The columns' left edges, from the same width rule the items size
326
+ * themselves by. These are the snap points, and they are what an item reads
327
+ * to know how far it is from the reading edge.
328
+ */
329
+ const offsets = useMemo(() => {
330
+ if (!horizontal) return [];
331
+ const result: number[] = [];
332
+ let sum = 0;
333
+ Children.forEach(children, (child) => {
334
+ if (!isValidElement(child)) return;
335
+ result.push(sum);
336
+ sum += itemWidth(child.props as TimelineItemProps);
337
+ });
338
+ return result;
339
+ }, [children, horizontal]);
340
+
341
+ const context = useMemo(
342
+ () => ({
343
+ activeStep: value,
344
+ variant,
345
+ orientation,
346
+ scrollX,
347
+ offsets,
348
+ animate: !reducedMotion,
349
+ }),
350
+ [value, variant, orientation, scrollX, offsets, reducedMotion]
351
+ );
352
+
353
+ const onScroll = useAnimatedScrollHandler((event) => {
354
+ scrollX.value = event.contentOffset.x;
355
+ });
356
+
357
+ if (!horizontal) {
358
+ return (
359
+ <TimelineContext.Provider value={context}>
360
+ <View ref={ref} className={root({ className })} {...props}>
361
+ {textChildren(children)}
362
+ </View>
363
+ </TimelineContext.Provider>
364
+ );
365
+ }
209
366
 
210
367
  return (
211
368
  <TimelineContext.Provider value={context}>
212
369
  <View ref={ref} className={root({ className })} {...props}>
213
- {textChildren(children)}
370
+ <AnimatedScrollView
371
+ horizontal
372
+ onScroll={onScroll}
373
+ scrollEventThrottle={16}
374
+ showsHorizontalScrollIndicator={false}
375
+ decelerationRate="fast"
376
+ snapToAlignment="start"
377
+ snapToOffsets={snap ? offsets : undefined}
378
+ >
379
+ {/*
380
+ The track sizes itself to its columns, so the rail stretched
381
+ between its own left and right edges runs the whole length of the
382
+ sequence rather than the width of the screen.
383
+ */}
384
+ <View className="relative flex-row">
385
+ <View
386
+ accessibilityElementsHidden
387
+ importantForAccessibility="no-hide-descendants"
388
+ pointerEvents="none"
389
+ style={{
390
+ position: 'absolute',
391
+ left: 0,
392
+ right: 0,
393
+ top: HORIZONTAL_RAIL_TOP,
394
+ borderTopWidth: 1,
395
+ // Dashed borders only render at a radius of zero, which this
396
+ // has, and the rule is the whole element.
397
+ borderStyle: 'dashed',
398
+ borderColor: typeof border === 'string' ? border : undefined,
399
+ }}
400
+ />
401
+ {textChildren(children)}
402
+ </View>
403
+ </AnimatedScrollView>
214
404
  </View>
215
405
  </TimelineContext.Provider>
216
406
  );
@@ -228,28 +418,88 @@ export interface TimelineItemProps extends ViewProps {
228
418
  tone?: TimelineTone;
229
419
  /** Set on the final item so its rail stops at the indicator. */
230
420
  last?: boolean;
421
+ /**
422
+ * Horizontal only: how wide this column is, in points.
423
+ *
424
+ * Left out, a column that carries content takes a readable width and one that
425
+ * carries none collapses to a tick — so a quiet stretch of the sequence
426
+ * compresses instead of paying full width for nothing. Set it to override
427
+ * that for a column that needs more or less room than its contents suggest.
428
+ */
429
+ width?: number;
231
430
  children?: ReactNode;
232
431
  }
233
432
 
234
433
  const TimelineItem = forwardRef<View, TimelineItemProps>(
235
434
  (
236
- { className, step, completed, tone = 'default', last = false, children, ...props },
435
+ {
436
+ className,
437
+ step,
438
+ completed,
439
+ tone = 'default',
440
+ last = false,
441
+ width,
442
+ children,
443
+ ...props
444
+ },
237
445
  ref
238
446
  ) => {
239
- const { activeStep, variant } = useTimeline('Timeline.Item');
447
+ const { activeStep, variant, orientation, scrollX, offsets, animate } =
448
+ useTimeline('Timeline.Item');
240
449
  const isCompleted = completed ?? step <= activeStep;
241
- const { item } = timelineVariants({ variant, tone, completed: isCompleted });
450
+ const { item } = timelineVariants({
451
+ variant,
452
+ tone,
453
+ completed: isCompleted,
454
+ orientation,
455
+ });
456
+ const horizontal = orientation === 'horizontal';
242
457
 
243
458
  const context = useMemo(
244
459
  () => ({ step, completed: isCompleted, tone, showSeparator: !last }),
245
460
  [step, isCompleted, tone, last]
246
461
  );
247
462
 
463
+ const columnWidth = horizontal
464
+ ? itemWidth({ step, width, children } as TimelineItemProps)
465
+ : undefined;
466
+ const offset = offsets[step] ?? 0;
467
+
468
+ /*
469
+ * A column recedes as it leaves the reading edge, so the one being read is
470
+ * the one that looks read. The window is a column's own width either side,
471
+ * which keeps the neighbours legible — this is a timeline, and the value of
472
+ * it is being able to see what came before and after at once.
473
+ */
474
+ const columnStyle = useAnimatedStyle(() => {
475
+ if (!horizontal || !animate) return { opacity: 1 };
476
+ const distance = Math.abs(scrollX.value - offset);
477
+ const window = Math.max(columnWidth ?? HORIZONTAL_WIDE, 1) * 2;
478
+ return {
479
+ opacity: interpolate(distance, [0, window], [1, 0.45], 'clamp'),
480
+ };
481
+ });
482
+
483
+ if (!horizontal) {
484
+ return (
485
+ <TimelineItemContext.Provider value={context}>
486
+ <View ref={ref} className={item({ className })} {...props}>
487
+ {textChildren(children)}
488
+ </View>
489
+ </TimelineItemContext.Provider>
490
+ );
491
+ }
492
+
248
493
  return (
249
494
  <TimelineItemContext.Provider value={context}>
250
- <View ref={ref} className={item({ className })} {...props}>
495
+ <Animated.View
496
+ ref={ref}
497
+ className={item({ className })}
498
+ style={[{ width: columnWidth }, columnStyle]}
499
+ {...props}
500
+ >
251
501
  {textChildren(children)}
252
- </View>
502
+ </Animated.View>
253
503
  </TimelineItemContext.Provider>
254
504
  );
255
505
  }
@@ -261,10 +511,28 @@ TimelineItem.displayName = 'Timeline.Item';
261
511
  * person. Place it before `Timeline.Indicator`.
262
512
  */
263
513
  const TimelineAside = forwardRef<View, ViewProps & { className?: string }>(
264
- ({ className, ...props }, ref) => {
265
- const { variant } = useTimeline('Timeline.Aside');
266
- const { aside } = timelineVariants({ variant });
267
- return <View ref={ref} className={aside({ className })} {...props} />;
514
+ ({ className, style, ...props }, ref) => {
515
+ const { variant, orientation } = useTimeline('Timeline.Aside');
516
+ const { aside } = timelineVariants({ variant, orientation });
517
+
518
+ return (
519
+ <View
520
+ ref={ref}
521
+ className={aside({ className })}
522
+ /*
523
+ * Horizontal: this band's height is where the rail lands, so it is
524
+ * fixed rather than sized to whatever the column happens to put in it.
525
+ * A column with a longer label would otherwise push its own tick below
526
+ * everybody else's and the rail would stop being a line.
527
+ */
528
+ style={
529
+ orientation === 'horizontal'
530
+ ? [{ height: HORIZONTAL_RAIL_TOP }, style]
531
+ : style
532
+ }
533
+ {...props}
534
+ />
535
+ );
268
536
  }
269
537
  );
270
538
  TimelineAside.displayName = 'Timeline.Aside';
@@ -284,15 +552,43 @@ export interface TimelineIndicatorProps extends ViewProps {
284
552
  */
285
553
  const TimelineIndicator = forwardRef<View, TimelineIndicatorProps>(
286
554
  ({ className, children, ...props }, ref) => {
287
- const { variant } = useTimeline('Timeline.Indicator');
555
+ const { variant, orientation } = useTimeline('Timeline.Indicator');
288
556
  const { step, completed, tone, showSeparator } =
289
557
  useTimelineItem('Timeline.Indicator');
290
558
  const { rail, indicator, indicatorLabel, separator } = timelineVariants({
291
559
  variant,
292
560
  tone,
293
561
  completed,
562
+ orientation,
294
563
  });
295
564
 
565
+ /*
566
+ * Horizontal: a tick sitting on the rail, not a node with a connector
567
+ * running out of it. The rail is already drawn across the whole track by
568
+ * the root, so what a column owes it is the mark saying where this entry
569
+ * falls — pulled up by half its height to straddle the line rather than
570
+ * hang off it.
571
+ */
572
+ if (orientation === 'horizontal') {
573
+ return (
574
+ <View className={rail()}>
575
+ <View
576
+ ref={ref}
577
+ accessibilityElementsHidden
578
+ importantForAccessibility="no-hide-descendants"
579
+ className={indicator({
580
+ className: `w-px rounded-none ${completed ? 'bg-primary' : 'bg-muted-foreground'} ${className ?? ''}`,
581
+ })}
582
+ style={{
583
+ height: HORIZONTAL_TICK_HEIGHT,
584
+ marginTop: -HORIZONTAL_TICK_HEIGHT / 2,
585
+ }}
586
+ {...props}
587
+ />
588
+ </View>
589
+ );
590
+ }
591
+
296
592
  // dot/card nodes are small discs with nothing inside, so they keep full
297
593
  // saturation — a soft tint at 16px would disappear. Nodes that hold an
298
594
  // icon or a number take the tinted fill instead.
@@ -352,9 +648,9 @@ TimelineIndicator.displayName = 'Timeline.Indicator';
352
648
  */
353
649
  const TimelineContent = forwardRef<View, ViewProps & { className?: string }>(
354
650
  ({ className, children, ...props }, ref) => {
355
- const { variant } = useTimeline('Timeline.Content');
651
+ const { variant, orientation } = useTimeline('Timeline.Content');
356
652
  const { completed, tone } = useTimelineItem('Timeline.Content');
357
- const { body, panel } = timelineVariants({ variant, tone, completed });
653
+ const { body, panel } = timelineVariants({ variant, tone, completed, orientation });
358
654
 
359
655
  return (
360
656
  <View ref={ref} className={body({ className })} {...props}>
@@ -368,8 +664,8 @@ TimelineContent.displayName = 'Timeline.Content';
368
664
  /** Title row: heading on the left, `Timeline.Trailing` on the right. */
369
665
  const TimelineHeader = forwardRef<View, ViewProps & { className?: string }>(
370
666
  ({ className, ...props }, ref) => {
371
- const { variant } = useTimeline('Timeline.Header');
372
- const { header } = timelineVariants({ variant });
667
+ const { variant, orientation } = useTimeline('Timeline.Header');
668
+ const { header } = timelineVariants({ variant, orientation });
373
669
  return <View ref={ref} className={header({ className })} {...props} />;
374
670
  }
375
671
  );
@@ -378,40 +674,40 @@ TimelineHeader.displayName = 'Timeline.Header';
378
674
  /** Wraps a title and anything stacked under it inside the header row. */
379
675
  const TimelineHeading = forwardRef<View, ViewProps & { className?: string }>(
380
676
  ({ className, ...props }, ref) => {
381
- const { variant } = useTimeline('Timeline.Heading');
382
- const { heading } = timelineVariants({ variant });
677
+ const { variant, orientation } = useTimeline('Timeline.Heading');
678
+ const { heading } = timelineVariants({ variant, orientation });
383
679
  return <View ref={ref} className={heading({ className })} {...props} />;
384
680
  }
385
681
  );
386
682
  TimelineHeading.displayName = 'Timeline.Heading';
387
683
 
388
684
  const TimelineDate = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
389
- const { variant } = useTimeline('Timeline.Date');
390
- const { date } = timelineVariants({ variant });
685
+ const { variant, orientation } = useTimeline('Timeline.Date');
686
+ const { date } = timelineVariants({ variant, orientation });
391
687
  return <Text ref={ref} className={date({ className })} {...props} />;
392
688
  });
393
689
  TimelineDate.displayName = 'Timeline.Date';
394
690
 
395
691
  /** Category line in the aside, coloured by the item's tone. */
396
692
  const TimelineLabel = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
397
- const { variant } = useTimeline('Timeline.Label');
693
+ const { variant, orientation } = useTimeline('Timeline.Label');
398
694
  const { tone } = useTimelineItem('Timeline.Label');
399
- const { label } = timelineVariants({ variant, tone });
695
+ const { label } = timelineVariants({ variant, tone, orientation });
400
696
  return <Text ref={ref} className={label({ className })} {...props} />;
401
697
  });
402
698
  TimelineLabel.displayName = 'Timeline.Label';
403
699
 
404
700
  /** Muted supporting line — a person's name, a source. */
405
701
  const TimelineMeta = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
406
- const { variant } = useTimeline('Timeline.Meta');
407
- const { meta } = timelineVariants({ variant });
702
+ const { variant, orientation } = useTimeline('Timeline.Meta');
703
+ const { meta } = timelineVariants({ variant, orientation });
408
704
  return <Text ref={ref} className={meta({ className })} {...props} />;
409
705
  });
410
706
  TimelineMeta.displayName = 'Timeline.Meta';
411
707
 
412
708
  const TimelineTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
413
- const { variant } = useTimeline('Timeline.Title');
414
- const { title } = timelineVariants({ variant });
709
+ const { variant, orientation } = useTimeline('Timeline.Title');
710
+ const { title } = timelineVariants({ variant, orientation });
415
711
  return <Text ref={ref} className={title({ className })} {...props} />;
416
712
  });
417
713
  TimelineTitle.displayName = 'Timeline.Title';
@@ -419,8 +715,8 @@ TimelineTitle.displayName = 'Timeline.Title';
419
715
  /** Right-hand slot in the header row — a timestamp, usually. */
420
716
  const TimelineTrailing = forwardRef<RNText, TextProps>(
421
717
  ({ className, ...props }, ref) => {
422
- const { variant } = useTimeline('Timeline.Trailing');
423
- const { trailing } = timelineVariants({ variant });
718
+ const { variant, orientation } = useTimeline('Timeline.Trailing');
719
+ const { trailing } = timelineVariants({ variant, orientation });
424
720
  return <Text ref={ref} className={trailing({ className })} {...props} />;
425
721
  }
426
722
  );
@@ -428,8 +724,8 @@ TimelineTrailing.displayName = 'Timeline.Trailing';
428
724
 
429
725
  const TimelineDescription = forwardRef<RNText, TextProps>(
430
726
  ({ className, ...props }, ref) => {
431
- const { variant } = useTimeline('Timeline.Description');
432
- const { content } = timelineVariants({ variant });
727
+ const { variant, orientation } = useTimeline('Timeline.Description');
728
+ const { content } = timelineVariants({ variant, orientation });
433
729
  return <Text ref={ref} className={content({ className })} {...props} />;
434
730
  }
435
731
  );
@@ -438,8 +734,8 @@ TimelineDescription.displayName = 'Timeline.Description';
438
734
  /** Bordered strip of label/value pairs under a title. */
439
735
  const TimelineStats = forwardRef<View, ViewProps & { className?: string }>(
440
736
  ({ className, ...props }, ref) => {
441
- const { variant } = useTimeline('Timeline.Stats');
442
- const { stats } = timelineVariants({ variant });
737
+ const { variant, orientation } = useTimeline('Timeline.Stats');
738
+ const { stats } = timelineVariants({ variant, orientation });
443
739
  return <View ref={ref} className={stats({ className })} {...props} />;
444
740
  }
445
741
  );
@@ -453,8 +749,8 @@ export interface TimelineStatProps extends ViewProps {
453
749
 
454
750
  const TimelineStat = forwardRef<View, TimelineStatProps>(
455
751
  ({ className, label, value, ...props }, ref) => {
456
- const { variant } = useTimeline('Timeline.Stat');
457
- const { statLabel, statValue } = timelineVariants({ variant });
752
+ const { variant, orientation } = useTimeline('Timeline.Stat');
753
+ const { statLabel, statValue } = timelineVariants({ variant, orientation });
458
754
  return (
459
755
  <View ref={ref} className={className} {...props}>
460
756
  <Text className={statLabel()}>{label}</Text>
@@ -333,6 +333,7 @@ const ToggleButtonRoot = forwardRef<View, ToggleButtonProps>(
333
333
  <IconColorProvider color={typeof tint === 'string' ? tint : undefined}>
334
334
  <AnimatedPressable
335
335
  ref={ref}
336
+ {...props}
336
337
  accessibilityRole="button"
337
338
  // `checked` is what a screen reader reads out as "on"/"off"; the
338
339
  // selected state is the entire point of this control, so it is
@@ -345,7 +346,6 @@ const ToggleButtonRoot = forwardRef<View, ToggleButtonProps>(
345
346
  slots.root({ className }),
346
347
  selected ? selectedClassName : unselectedClassName
347
348
  )}
348
- {...props}
349
349
  >
350
350
  {textChildren(children, (text) => (
351
351
  <Text className={slots.label({ className: labelClassName })}>
@@ -54,7 +54,13 @@ import {
54
54
  useState,
55
55
  type ReactNode,
56
56
  } from 'react';
57
- import { Pressable, View, type Text as RNText, type ViewProps } from 'react-native';
57
+ import {
58
+ Pressable,
59
+ View,
60
+ type PressableProps,
61
+ type Text as RNText,
62
+ type ViewProps,
63
+ } from 'react-native';
58
64
  import Animated, {
59
65
  LinearTransition,
60
66
  useAnimatedStyle,
@@ -353,10 +359,9 @@ const TreeItem = forwardRef<View, TreeItemProps>(
353
359
  );
354
360
  TreeItem.displayName = 'Tree.Item';
355
361
 
356
- export interface TreeTriggerProps extends ViewProps {
362
+ /** Pressable props are forwarded; `onPress` runs after the tree updates its state. */
363
+ export interface TreeTriggerProps extends Omit<PressableProps, 'children'> {
357
364
  className?: string;
358
- /** Runs after the press has been handled, with the tree's own state already updated. */
359
- onPress?: () => void;
360
365
  children?: ReactNode;
361
366
  }
362
367
 
@@ -370,20 +375,20 @@ export interface TreeTriggerProps extends ViewProps {
370
375
  * actually survives to the user.
371
376
  */
372
377
  const TreeTrigger = forwardRef<View, TreeTriggerProps>(
373
- ({ className, onPress, children, ...props }, ref) => {
378
+ ({ className, onPress, disabled = false, children, ...props }, ref) => {
374
379
  const { toggleExpanded, select, selectionMode, expandOnPress, size } =
375
380
  useTree('Tree.Trigger');
376
381
  const { value, level, isExpanded, isSelected, isDisabled, isBranch } =
377
382
  useTreeItem('Tree.Trigger');
378
- const { trigger } = treeVariants({ size, isSelected, isDisabled });
379
-
380
383
  const accentForeground = useCSSVariable('--color-accent-foreground');
381
384
  const selectedColor = typeof accentForeground === 'string' ? accentForeground : undefined;
382
385
 
383
- const handlePress = () => {
386
+ const triggerDisabled = Boolean(isDisabled || disabled);
387
+ const { trigger } = treeVariants({ size, isSelected, isDisabled: triggerDisabled });
388
+ const handlePress: NonNullable<PressableProps['onPress']> = (event) => {
384
389
  if (isBranch && expandOnPress) toggleExpanded(value);
385
390
  select(value);
386
- onPress?.();
391
+ onPress?.(event);
387
392
  };
388
393
 
389
394
  return (
@@ -393,13 +398,13 @@ const TreeTrigger = forwardRef<View, TreeTriggerProps>(
393
398
  accessibilityState={{
394
399
  expanded: isBranch ? isExpanded : undefined,
395
400
  selected: selectionMode === 'none' ? undefined : isSelected,
396
- disabled: isDisabled,
401
+ disabled: triggerDisabled,
397
402
  }}
398
403
  aria-level={level + 1}
399
- disabled={isDisabled}
400
- onPress={handlePress}
404
+ disabled={triggerDisabled}
401
405
  className={trigger({ className })}
402
406
  {...props}
407
+ onPress={handlePress}
403
408
  >
404
409
  <IconColorProvider color={isSelected ? selectedColor : undefined}>
405
410
  {children}
@@ -45,7 +45,12 @@
45
45
  * ```
46
46
  */
47
47
  import { useCallback, useEffect, useState } from 'react';
48
- import { useWindowDimensions, type LayoutChangeEvent, type View } from 'react-native';
48
+ import {
49
+ useWindowDimensions,
50
+ type LayoutChangeEvent,
51
+ type View,
52
+ type ViewStyle,
53
+ } from 'react-native';
49
54
  import {
50
55
  measure,
51
56
  runOnJS,
@@ -106,7 +111,7 @@ export interface UseKeyboardAvoidanceResult {
106
111
  /** Attach to the same element, so a re-layout at rest cannot leave it offset. */
107
112
  onLayout: (event: LayoutChangeEvent) => void;
108
113
  /** Apply to the same element. */
109
- animatedStyle: ReturnType<typeof useAnimatedStyle>;
114
+ animatedStyle: ReturnType<typeof useAnimatedStyle<ViewStyle>>;
110
115
  }
111
116
 
112
117
  type KeyboardHeightHook = () => SharedValue<number>;
package/src/index.ts CHANGED
@@ -522,6 +522,7 @@ export {
522
522
  type NumberInputProps,
523
523
  } from './components/number-input';
524
524
  export { OtpInput, type OtpInputProps } from './components/otp-input';
525
+ export { SearchBar, type SearchBarProps } from './components/search-bar';
525
526
  export {
526
527
  Panelside,
527
528
  usePanelside,
@@ -1068,6 +1069,7 @@ export {
1068
1069
  type TimelineStatProps,
1069
1070
  type TimelineVariant,
1070
1071
  type TimelineTone,
1072
+ type TimelineOrientation,
1071
1073
  } from './components/timeline';
1072
1074
  export {
1073
1075
  Toast,