@boyernick/standard-ui-react 0.1.1-canary.9 → 0.2.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 (82) hide show
  1. package/package.json +5 -3
  2. package/src/accordion.tsx +5 -2
  3. package/src/alert-dialog.tsx +1 -1
  4. package/src/attachment.tsx +5 -2
  5. package/src/autocomplete.tsx +4 -3
  6. package/src/block-editor.tsx +1747 -0
  7. package/src/brand.tsx +2 -2
  8. package/src/breadcrumb.tsx +14 -7
  9. package/src/button.tsx +22 -11
  10. package/src/calendar.tsx +6 -3
  11. package/src/carousel.tsx +220 -53
  12. package/src/checkbox.tsx +3 -2
  13. package/src/code-block.tsx +66 -5
  14. package/src/collapsible.tsx +4 -2
  15. package/src/combobox.tsx +4 -3
  16. package/src/command.tsx +27 -12
  17. package/src/dialog.tsx +33 -10
  18. package/src/empty.tsx +4 -4
  19. package/src/field.tsx +4 -3
  20. package/src/filter-group.tsx +143 -0
  21. package/src/icons.tsx +28 -1
  22. package/src/illustrations.tsx +219 -141
  23. package/src/index.ts +238 -18
  24. package/src/input.tsx +8 -5
  25. package/src/kbd.tsx +10 -4
  26. package/src/lib/focus.ts +41 -0
  27. package/src/lib/popup.ts +1 -1
  28. package/src/lifeline/company-icon.tsx +71 -0
  29. package/src/lifeline/index.ts +42 -0
  30. package/src/lifeline/lifeline-data.ts +97 -0
  31. package/src/lifeline/lifeline-desktop.tsx +204 -0
  32. package/src/lifeline/lifeline-event.tsx +108 -0
  33. package/src/lifeline/lifeline-fireworks.tsx +302 -0
  34. package/src/lifeline/lifeline-hover-image.tsx +269 -0
  35. package/src/lifeline/lifeline-icons.tsx +39 -0
  36. package/src/lifeline/lifeline-intro-timing.ts +105 -0
  37. package/src/lifeline/lifeline-labels.tsx +24 -0
  38. package/src/lifeline/lifeline-layout.ts +1 -0
  39. package/src/lifeline/lifeline-legend.tsx +31 -0
  40. package/src/lifeline/lifeline-lightbox.tsx +309 -0
  41. package/src/lifeline/lifeline-marker.tsx +183 -0
  42. package/src/lifeline/lifeline-people.tsx +99 -0
  43. package/src/lifeline/lifeline-photos.tsx +366 -0
  44. package/src/lifeline/lifeline-shell.tsx +135 -0
  45. package/src/lifeline/lifeline-utils.ts +83 -0
  46. package/src/lifeline/lifeline-vertical.tsx +482 -0
  47. package/src/lifeline/lifeline.tsx +69 -0
  48. package/src/lifeline/types.ts +112 -0
  49. package/src/lifeline/use-lifeline-intro.ts +130 -0
  50. package/src/lifeline/use-lifeline-scroll.ts +1011 -0
  51. package/src/lifeline/use-lifeline-vertical-scroll.ts +271 -0
  52. package/src/menubar.tsx +9 -2
  53. package/src/minimap.tsx +296 -0
  54. package/src/modal.tsx +608 -0
  55. package/src/navigation-menu.tsx +338 -67
  56. package/src/number-field.tsx +336 -61
  57. package/src/otp-field.tsx +4 -3
  58. package/src/pagination.tsx +262 -42
  59. package/src/password-protection.tsx +345 -0
  60. package/src/popover.tsx +1 -1
  61. package/src/progress.tsx +5 -0
  62. package/src/questionnaire.tsx +284 -0
  63. package/src/radio.tsx +4 -2
  64. package/src/scroll-area.tsx +4 -1
  65. package/src/select.tsx +5 -2
  66. package/src/sidebar.tsx +113 -28
  67. package/src/slider.tsx +66 -6
  68. package/src/sounds.tsx +6 -10
  69. package/src/spinner.tsx +105 -32
  70. package/src/switch.tsx +4 -1
  71. package/src/table.tsx +82 -15
  72. package/src/tabs.tsx +189 -38
  73. package/src/text-animate.tsx +71 -28
  74. package/src/textarea.tsx +6 -1
  75. package/src/timeline.tsx +378 -0
  76. package/src/toast.tsx +214 -35
  77. package/src/toggle.tsx +4 -2
  78. package/src/toolbar.tsx +12 -7
  79. package/src/tooltip.tsx +43 -3
  80. package/src/video-player.tsx +396 -127
  81. package/src/image-modal.tsx +0 -110
  82. package/src/markdown-editor.tsx +0 -302
@@ -8,12 +8,18 @@ import {
8
8
  } from "react"
9
9
  import { cn } from "./lib/cn"
10
10
 
11
+ /** How many glyphs are scrambling at once. The window travels the line; behind
12
+ * it the text is settled, ahead of it nothing is drawn yet. 8 is the reference
13
+ * default from baffle.js, which this effect follows. */
14
+ const DECODE_WINDOW = 8
15
+
11
16
  const DECODE_GLYPHS =
12
17
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#@$%&*"
13
18
 
14
19
  export type TextAnimateEffect =
15
20
  | "typewriter"
16
21
  | "decode"
22
+ | "shimmer"
17
23
  | "fade"
18
24
  | "blur"
19
25
 
@@ -32,20 +38,25 @@ export type TextAnimateProps = HTMLAttributes<HTMLSpanElement> & {
32
38
  export const TextAnimate = ({
33
39
  text,
34
40
  effect = "typewriter",
35
- speed = 40,
41
+ speed = 60,
36
42
  delay = 0,
37
43
  replay = true,
38
44
  as: Tag = "span",
39
45
  className,
40
46
  ...props
41
47
  }: TextAnimateProps) => {
42
- const [output, setOutput] = useState(effect === "typewriter" || effect === "decode" ? "" : text)
43
- const [done, setDone] = useState(effect === "fade" || effect === "blur")
48
+ const [output, setOutput] = useState(
49
+ effect === "typewriter" || effect === "decode" ? "" : text,
50
+ )
51
+ const [done, setDone] = useState(
52
+ effect === "fade" || effect === "blur" || effect === "shimmer",
53
+ )
44
54
  const chars = useMemo(() => [...text], [text])
45
55
 
46
56
  useEffect(() => {
47
57
  let cancelled = false
48
58
  let timeoutId: ReturnType<typeof setTimeout> | undefined
59
+ let frameId: number | undefined
49
60
 
50
61
  const start = () => {
51
62
  if (
@@ -57,7 +68,7 @@ export const TextAnimate = ({
57
68
  return
58
69
  }
59
70
 
60
- if (effect === "fade" || effect === "blur") {
71
+ if (effect === "fade" || effect === "blur" || effect === "shimmer") {
61
72
  setOutput(text)
62
73
  setDone(true)
63
74
  return
@@ -82,30 +93,59 @@ export const TextAnimate = ({
82
93
  return
83
94
  }
84
95
 
85
- // decode
86
- setOutput(
87
- chars.map(() => DECODE_GLYPHS[Math.floor(Math.random() * DECODE_GLYPHS.length)]).join(""),
88
- )
89
- let revealed = 0
90
- const tick = () => {
96
+ // Decode follows baffle.js's `reveal`, which is the reference for this
97
+ // effect: a fixed-width window of scrambled glyphs travels left to
98
+ // right, the real text sits behind it, and nothing at all is drawn ahead
99
+ // of it — so the line grows in with a churning leading edge rather than
100
+ // resolving out of a full-width block of noise.
101
+ //
102
+ // It runs on rAF with a time accumulator rather than `setTimeout(speed)`.
103
+ // A 28ms timer does not divide into a ~16.7ms frame, so steps land on
104
+ // uneven frames and the text visibly stutters; accumulating elapsed time
105
+ // keeps the reference's cadence while landing each step on a real frame.
106
+ const slots = chars.reduce<number[]>((acc, char, index) => {
107
+ if (char !== " ") acc.push(index)
108
+ return acc
109
+ }, [])
110
+
111
+ // The window starts off the front of the line so the first glyph is
112
+ // already scrambling as it arrives.
113
+ let position = -DECODE_WINDOW
114
+ let previousStep = -Infinity
115
+ const startedAt = performance.now()
116
+
117
+ const frame = (now: number) => {
91
118
  if (cancelled) return
92
- revealed += 1
93
- const next = chars
94
- .map((char, index) => {
95
- if (char === " ") return " "
96
- if (index < revealed) return char
97
- return DECODE_GLYPHS[Math.floor(Math.random() * DECODE_GLYPHS.length)]
98
- })
99
- .join("")
100
- setOutput(next)
101
- if (revealed < chars.length) {
102
- timeoutId = setTimeout(tick, speed)
103
- } else {
104
- setOutput(text)
105
- setDone(true)
119
+
120
+ const step = Math.floor((now - startedAt) / speed) - DECODE_WINDOW
121
+ if (step !== previousStep) {
122
+ previousStep = step
123
+ position = step
124
+
125
+ if (position > slots.length) {
126
+ setOutput(text)
127
+ setDone(true)
128
+ return
129
+ }
130
+
131
+ const next = [...chars]
132
+ for (let p = Math.max(position, 0); p < slots.length; p += 1) {
133
+ next[slots[p]] =
134
+ p < position + DECODE_WINDOW
135
+ ? DECODE_GLYPHS[
136
+ Math.floor(Math.random() * DECODE_GLYPHS.length)
137
+ ]
138
+ : ""
139
+ }
140
+ setOutput(next.join(""))
106
141
  }
142
+
143
+ frameId = requestAnimationFrame(frame)
107
144
  }
108
- timeoutId = setTimeout(tick, speed)
145
+
146
+ setOutput("")
147
+ frameId = requestAnimationFrame(frame)
148
+ return
109
149
  }
110
150
 
111
151
  timeoutId = setTimeout(start, delay)
@@ -113,18 +153,21 @@ export const TextAnimate = ({
113
153
  return () => {
114
154
  cancelled = true
115
155
  if (timeoutId) clearTimeout(timeoutId)
156
+ if (frameId) cancelAnimationFrame(frameId)
116
157
  }
117
158
  }, [chars, delay, effect, replay, speed, text])
118
159
 
119
- if (effect === "fade" || effect === "blur") {
160
+ if (effect === "fade" || effect === "blur" || effect === "shimmer") {
120
161
  return (
121
162
  <Tag
122
163
  className={cn(
123
164
  "inline-block text-fg-primary",
124
165
  effect === "fade" &&
125
- "animate-[text-fade-in_0.6s_var(--ease-enter)_both] motion-reduce:animate-none",
166
+ "animate-[text-fade-in_0.9s_var(--ease-enter)_both] motion-reduce:animate-none",
126
167
  effect === "blur" &&
127
- "animate-[text-blur-in_0.7s_var(--ease-enter)_both] motion-reduce:animate-none",
168
+ "animate-[text-blur-in_1s_var(--ease-enter)_both] motion-reduce:animate-none",
169
+ effect === "shimmer" &&
170
+ "bg-[linear-gradient(110deg,var(--color-fg-tertiary)_35%,var(--color-fg-primary)_50%,var(--color-fg-tertiary)_65%)] bg-[length:250%_100%] bg-clip-text text-transparent animate-[text-shimmer_3s_linear_infinite] motion-reduce:animate-none motion-reduce:bg-none motion-reduce:text-fg-primary",
128
171
  className,
129
172
  )}
130
173
  style={{ animationDelay: `${delay}ms` }}
package/src/textarea.tsx CHANGED
@@ -1,9 +1,14 @@
1
1
  import { cva, type VariantProps } from "class-variance-authority"
2
2
  import type { TextareaHTMLAttributes } from "react"
3
3
  import { cn } from "./lib/cn"
4
+ import { focusRing, focusRingInvalid } from "./lib/focus"
4
5
 
5
6
  const textareaVariants = cva(
6
- "flex min-h-20 w-full cursor-text resize-y rounded-md px-3 py-2 text-sm text-fg-primary transition-[color,box-shadow] duration-[var(--duration-sm)] ease-enter placeholder:text-fg-quaternary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50",
7
+ cn(
8
+ "flex min-h-20 w-full cursor-text resize-y rounded-md px-3 py-2 text-sm text-fg-primary transition-[color,box-shadow] duration-[var(--duration-sm)] ease-enter placeholder:text-fg-quaternary disabled:cursor-not-allowed disabled:opacity-50",
9
+ focusRing,
10
+ focusRingInvalid,
11
+ ),
7
12
  {
8
13
  variants: {
9
14
  variant: {
@@ -0,0 +1,378 @@
1
+ "use client"
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import {
5
+ createContext,
6
+ type ComponentProps,
7
+ type KeyboardEvent,
8
+ type WheelEvent,
9
+ useContext,
10
+ } from "react"
11
+ import { cn } from "./lib/cn"
12
+ import { focusRing, focusRingBorder } from "./lib/focus"
13
+ import { motion } from "./lib/motion"
14
+
15
+ const timelineVariants = cva("group/timeline relative text-fg-primary", {
16
+ variants: {
17
+ orientation: {
18
+ vertical: "w-full",
19
+ horizontal: cn(
20
+ "w-full min-h-0 overflow-x-auto overscroll-x-contain scroll-smooth [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden focus-visible:rounded-md",
21
+ focusRingBorder,
22
+ focusRing,
23
+ ),
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ orientation: "vertical",
28
+ },
29
+ })
30
+
31
+ const timelineTrackVariants = cva("relative m-0 list-none p-0", {
32
+ variants: {
33
+ orientation: {
34
+ vertical:
35
+ "before:pointer-events-none before:absolute before:top-0 before:bottom-0 before:left-[calc(4.5rem+0.75rem+0.5rem)] before:w-px before:-translate-x-1/2 before:border-l before:border-dashed before:border-border-primary before:content-['']",
36
+ horizontal:
37
+ "flex w-max items-start before:pointer-events-none before:absolute before:inset-x-0 before:top-[var(--timeline-rail)] before:border-t before:border-dashed before:border-border-primary before:content-['']",
38
+ },
39
+ },
40
+ defaultVariants: {
41
+ orientation: "vertical",
42
+ },
43
+ })
44
+
45
+ const timelineItemVariants = cva("group/timeline-item relative", {
46
+ variants: {
47
+ orientation: {
48
+ vertical: "grid grid-cols-[4.5rem_1rem_minmax(0,1fr)] gap-x-3 pb-8 last:pb-0",
49
+ horizontal:
50
+ "w-[min(18rem,70vw)] shrink-0 pr-10 last:pr-0 [--timeline-rail:2.75rem]",
51
+ },
52
+ },
53
+ defaultVariants: {
54
+ orientation: "vertical",
55
+ },
56
+ })
57
+
58
+ const timelineMarkerVariants = cva("z-[1] shrink-0", {
59
+ variants: {
60
+ orientation: {
61
+ vertical: "col-start-2 row-start-1 mt-2 justify-self-center",
62
+ horizontal:
63
+ "absolute top-[var(--timeline-rail)] left-0 -translate-y-1/2",
64
+ },
65
+ shape: {
66
+ tick: "bg-fg-quaternary group-hover/timeline-item:bg-fg-tertiary",
67
+ dot: "size-2.5 rounded-full border-2 border-background-primary shadow-hairline",
68
+ },
69
+ tone: {
70
+ neutral: "",
71
+ accent: "",
72
+ info: "",
73
+ success: "",
74
+ warning: "",
75
+ critical: "",
76
+ },
77
+ },
78
+ compoundVariants: [
79
+ {
80
+ shape: "tick",
81
+ orientation: "vertical",
82
+ class: "h-px w-2.5",
83
+ },
84
+ {
85
+ shape: "tick",
86
+ orientation: "horizontal",
87
+ class: "h-2.5 w-px",
88
+ },
89
+ { shape: "tick", tone: "neutral", class: "bg-fg-quaternary" },
90
+ { shape: "tick", tone: "accent", class: "bg-brand-primary" },
91
+ { shape: "tick", tone: "info", class: "bg-status-info" },
92
+ { shape: "tick", tone: "success", class: "bg-status-success" },
93
+ { shape: "tick", tone: "warning", class: "bg-status-warning" },
94
+ { shape: "tick", tone: "critical", class: "bg-status-critical" },
95
+ { shape: "dot", tone: "neutral", class: "bg-fg-tertiary" },
96
+ { shape: "dot", tone: "accent", class: "bg-brand-primary" },
97
+ { shape: "dot", tone: "info", class: "bg-status-info" },
98
+ { shape: "dot", tone: "success", class: "bg-status-success" },
99
+ { shape: "dot", tone: "warning", class: "bg-status-warning" },
100
+ { shape: "dot", tone: "critical", class: "bg-status-critical" },
101
+ ],
102
+ defaultVariants: {
103
+ orientation: "vertical",
104
+ shape: "tick",
105
+ tone: "neutral",
106
+ },
107
+ })
108
+
109
+ const timelineTimeVariants = cva(
110
+ cn(
111
+ "text-sm-strong block tabular-nums text-fg-secondary",
112
+ motion.colors,
113
+ "group-hover/timeline-item:text-fg-primary",
114
+ ),
115
+ {
116
+ variants: {
117
+ orientation: {
118
+ vertical: "col-start-1 row-start-1 pt-0.5 text-right leading-5",
119
+ horizontal: "mb-6 h-5 leading-5",
120
+ },
121
+ },
122
+ defaultVariants: {
123
+ orientation: "vertical",
124
+ },
125
+ },
126
+ )
127
+
128
+ type TimelineOrientation = "horizontal" | "vertical"
129
+
130
+ const TimelineContext = createContext<TimelineOrientation>("vertical")
131
+
132
+ export type TimelineProps = ComponentProps<"div"> &
133
+ VariantProps<typeof timelineVariants> & {
134
+ /** Distance moved by the horizontal timeline's arrow keys. */
135
+ scrollStep?: number
136
+ }
137
+
138
+ export type TimelineTrackProps = ComponentProps<"ol">
139
+ export type TimelineItemProps = ComponentProps<"li">
140
+ export type TimelineMarkerProps = ComponentProps<"span"> &
141
+ VariantProps<typeof timelineMarkerVariants>
142
+ export type TimelineTimeProps = ComponentProps<"time">
143
+ export type TimelineContentProps = ComponentProps<"div">
144
+ export type TimelineTitleProps = ComponentProps<"h3">
145
+ export type TimelineDescriptionProps = ComponentProps<"p">
146
+ export type TimelineMediaProps = ComponentProps<"div">
147
+
148
+ export const Timeline = ({
149
+ orientation = "vertical",
150
+ scrollStep = 256,
151
+ className,
152
+ children,
153
+ tabIndex,
154
+ onKeyDown,
155
+ onWheel,
156
+ "aria-label": ariaLabel = "Timeline",
157
+ ...props
158
+ }: TimelineProps) => {
159
+ const resolvedOrientation = orientation ?? "vertical"
160
+
161
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
162
+ onKeyDown?.(event)
163
+ if (
164
+ event.defaultPrevented ||
165
+ resolvedOrientation !== "horizontal" ||
166
+ event.target !== event.currentTarget
167
+ ) {
168
+ return
169
+ }
170
+
171
+ const node = event.currentTarget
172
+ const prefersReducedMotion = window.matchMedia(
173
+ "(prefers-reduced-motion: reduce)",
174
+ ).matches
175
+ const behavior = prefersReducedMotion ? "auto" : "smooth"
176
+
177
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
178
+ event.preventDefault()
179
+ node.scrollBy({
180
+ left: event.key === "ArrowRight" ? scrollStep : -scrollStep,
181
+ behavior,
182
+ })
183
+ }
184
+
185
+ if (event.key === "Home" || event.key === "End") {
186
+ event.preventDefault()
187
+ node.scrollTo({
188
+ left: event.key === "Home" ? 0 : node.scrollWidth,
189
+ behavior,
190
+ })
191
+ }
192
+ }
193
+
194
+ const handleWheel = (event: WheelEvent<HTMLDivElement>) => {
195
+ onWheel?.(event)
196
+ if (
197
+ event.defaultPrevented ||
198
+ resolvedOrientation !== "horizontal" ||
199
+ Math.abs(event.deltaX) >= Math.abs(event.deltaY)
200
+ ) {
201
+ return
202
+ }
203
+
204
+ const node = event.currentTarget
205
+ const maxScrollLeft = node.scrollWidth - node.clientWidth
206
+ const canScroll =
207
+ event.deltaY > 0 ? node.scrollLeft < maxScrollLeft : node.scrollLeft > 0
208
+
209
+ if (canScroll) {
210
+ event.preventDefault()
211
+ node.scrollLeft += event.deltaY
212
+ }
213
+ }
214
+
215
+ return (
216
+ <TimelineContext.Provider value={resolvedOrientation}>
217
+ <div
218
+ data-slot="timeline"
219
+ data-orientation={resolvedOrientation}
220
+ role="region"
221
+ aria-label={ariaLabel}
222
+ tabIndex={
223
+ tabIndex ?? (resolvedOrientation === "horizontal" ? 0 : undefined)
224
+ }
225
+ className={cn(
226
+ timelineVariants({ orientation: resolvedOrientation }),
227
+ className,
228
+ )}
229
+ onKeyDown={handleKeyDown}
230
+ onWheel={handleWheel}
231
+ {...props}
232
+ >
233
+ {children}
234
+ </div>
235
+ </TimelineContext.Provider>
236
+ )
237
+ }
238
+
239
+ export const TimelineTrack = ({
240
+ className,
241
+ ...props
242
+ }: TimelineTrackProps) => {
243
+ const orientation = useContext(TimelineContext)
244
+
245
+ return (
246
+ <ol
247
+ data-slot="timeline-track"
248
+ data-orientation={orientation}
249
+ className={cn(
250
+ timelineTrackVariants({ orientation }),
251
+ orientation === "horizontal" && "[--timeline-rail:2.75rem]",
252
+ className,
253
+ )}
254
+ {...props}
255
+ />
256
+ )
257
+ }
258
+
259
+ export const TimelineItem = ({
260
+ className,
261
+ ...props
262
+ }: TimelineItemProps) => {
263
+ const orientation = useContext(TimelineContext)
264
+
265
+ return (
266
+ <li
267
+ data-slot="timeline-item"
268
+ data-orientation={orientation}
269
+ className={cn(timelineItemVariants({ orientation }), className)}
270
+ {...props}
271
+ />
272
+ )
273
+ }
274
+
275
+ export const TimelineMarker = ({
276
+ className,
277
+ tone,
278
+ shape,
279
+ ...props
280
+ }: TimelineMarkerProps) => {
281
+ const orientation = useContext(TimelineContext)
282
+
283
+ return (
284
+ <span
285
+ data-slot="timeline-marker"
286
+ aria-hidden
287
+ className={cn(
288
+ timelineMarkerVariants({ orientation, shape, tone }),
289
+ motion.colors,
290
+ className,
291
+ )}
292
+ {...props}
293
+ />
294
+ )
295
+ }
296
+
297
+ export const TimelineTime = ({
298
+ className,
299
+ ...props
300
+ }: TimelineTimeProps) => {
301
+ const orientation = useContext(TimelineContext)
302
+
303
+ return (
304
+ <time
305
+ data-slot="timeline-time"
306
+ className={cn(timelineTimeVariants({ orientation }), className)}
307
+ {...props}
308
+ />
309
+ )
310
+ }
311
+
312
+ export const TimelineContent = ({
313
+ className,
314
+ ...props
315
+ }: TimelineContentProps) => {
316
+ const orientation = useContext(TimelineContext)
317
+
318
+ return (
319
+ <div
320
+ data-slot="timeline-content"
321
+ className={cn(
322
+ "min-w-0",
323
+ orientation === "vertical" && "col-start-3 row-start-1",
324
+ orientation === "horizontal" && "pt-6",
325
+ className,
326
+ )}
327
+ {...props}
328
+ />
329
+ )
330
+ }
331
+
332
+ export const TimelineTitle = ({
333
+ className,
334
+ ...props
335
+ }: TimelineTitleProps) => (
336
+ <h3
337
+ data-slot="timeline-title"
338
+ className={cn(
339
+ "text-sm-strong text-fg-primary",
340
+ motion.colors,
341
+ className,
342
+ )}
343
+ {...props}
344
+ />
345
+ )
346
+
347
+ export const TimelineDescription = ({
348
+ className,
349
+ ...props
350
+ }: TimelineDescriptionProps) => (
351
+ <p
352
+ data-slot="timeline-description"
353
+ className={cn("text-sm mt-1 leading-relaxed text-fg-secondary", className)}
354
+ {...props}
355
+ />
356
+ )
357
+
358
+ export const TimelineMedia = ({
359
+ className,
360
+ ...props
361
+ }: TimelineMediaProps) => (
362
+ <div
363
+ data-slot="timeline-media"
364
+ className={cn(
365
+ "mt-4 overflow-hidden rounded-xl ring-1 ring-border-primary [&>img]:block [&>img]:aspect-4/3 [&>img]:w-full [&>img]:object-cover",
366
+ className,
367
+ )}
368
+ {...props}
369
+ />
370
+ )
371
+
372
+ export {
373
+ timelineVariants,
374
+ timelineItemVariants,
375
+ timelineMarkerVariants,
376
+ timelineTimeVariants,
377
+ timelineTrackVariants,
378
+ }