trix-ui 0.2.2 → 0.2.4

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.
@@ -0,0 +1,92 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export type SignatureHeroProps = {
6
+ eyebrow?: string
7
+ title?: string
8
+ description?: string
9
+ primaryCta?: string
10
+ secondaryCta?: string
11
+ className?: string
12
+ }
13
+
14
+ export function SignatureHero({
15
+ eyebrow = "Signature",
16
+ title = "Design systems with editorial polish",
17
+ description = "Layered layouts, refined typography, and restrained motion for premium product experiences.",
18
+ primaryCta = "Get started",
19
+ secondaryCta = "View docs",
20
+ className
21
+ }: SignatureHeroProps) {
22
+ return (
23
+ <section
24
+ className={cn(
25
+ "relative overflow-hidden rounded-[32px] border border-border bg-background p-8 shadow-[0_30px_80px_-60px_rgba(15,23,42,0.6)] md:p-12",
26
+ className
27
+ )}
28
+ >
29
+ <div className="absolute inset-0">
30
+ <div className="absolute -left-24 -top-24 h-64 w-64 rounded-full bg-primary/15 blur-3xl" />
31
+ <div className="absolute -bottom-24 -right-24 h-72 w-72 rounded-full bg-muted/30 blur-3xl" />
32
+ </div>
33
+
34
+ <div className="relative z-10 grid gap-10 md:grid-cols-[1.2fr_0.8fr]">
35
+ <div className="space-y-5">
36
+ <p className="text-xs font-semibold uppercase tracking-[0.3em] text-muted-foreground">
37
+ {eyebrow}
38
+ </p>
39
+ <h1 className="text-3xl font-semibold tracking-tight text-foreground md:text-4xl">
40
+ {title}
41
+ </h1>
42
+ <p className="max-w-xl text-sm text-muted-foreground md:text-base">
43
+ {description}
44
+ </p>
45
+ <div className="flex flex-wrap items-center gap-3 pt-2">
46
+ <button
47
+ type="button"
48
+ className="rounded-full bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground shadow-sm"
49
+ >
50
+ {primaryCta}
51
+ </button>
52
+ <button
53
+ type="button"
54
+ className="rounded-full border border-border px-6 py-2 text-sm font-semibold text-foreground"
55
+ >
56
+ {secondaryCta}
57
+ </button>
58
+ </div>
59
+ </div>
60
+
61
+ <div className="grid gap-4">
62
+ {[
63
+ {
64
+ label: "Typography",
65
+ value: "System-led type scales"
66
+ },
67
+ {
68
+ label: "Layout",
69
+ value: "Adaptive grid + spacing"
70
+ },
71
+ {
72
+ label: "Motion",
73
+ value: "Subtle, purposeful animations"
74
+ }
75
+ ].map((item) => (
76
+ <div
77
+ key={item.label}
78
+ className="rounded-2xl border border-border/60 bg-card/70 p-4 backdrop-blur"
79
+ >
80
+ <p className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
81
+ {item.label}
82
+ </p>
83
+ <p className="mt-2 text-sm font-semibold text-foreground">
84
+ {item.value}
85
+ </p>
86
+ </div>
87
+ ))}
88
+ </div>
89
+ </div>
90
+ </section>
91
+ )
92
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { Slot } from "@radix-ui/react-slot";
5
- import { cn } from "../../lib/utils";
5
+ import { cn } from "@/lib/utils";
6
6
 
7
7
  type AnyElement = HTMLElement;
8
8
 
@@ -0,0 +1,248 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../../lib/utils";
5
+
6
+ type Axis = "y" | "x";
7
+
8
+ export type ScrollProgressState = {
9
+ /** 0..1 (clamped if clamp=true) */
10
+ progress: number;
11
+ /** Raw (can go <0 or >1 when clamp=false) */
12
+ rawProgress: number;
13
+ /** True if any part of the element intersects the viewport */
14
+ inView: boolean;
15
+ /** Element rect in viewport coordinates */
16
+ rect: DOMRect | null;
17
+ };
18
+
19
+ export interface ScrollProgressWrapProps
20
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "onProgress"> {
21
+ /** Render-prop child gets scroll progress state */
22
+ children: (state: ScrollProgressState) => React.ReactNode;
23
+
24
+ /** Scroll axis (default "y") */
25
+ axis?: Axis;
26
+
27
+ /**
28
+ * When progress should start/end.
29
+ * - "cover": progress=0 when element just enters, progress=1 when it just leaves (default)
30
+ * - "contain": progress=0 when element top hits viewport top, progress=1 when bottom hits viewport bottom
31
+ */
32
+ mode?: "cover" | "contain";
33
+
34
+ /** Clamp progress to 0..1 (default true) */
35
+ clamp?: boolean;
36
+
37
+ /**
38
+ * Call on every progress change (rAF throttled)
39
+ * Receives clamped progress.
40
+ */
41
+ onProgress?: (progress: number, state: ScrollProgressState) => void;
42
+
43
+ /**
44
+ * Additional offset (px) applied to viewport size.
45
+ * Useful to trigger earlier/later without rootMargin complexity.
46
+ * Default 0.
47
+ */
48
+ viewportOffset?: number;
49
+
50
+ /**
51
+ * Update frequency control. If true (default), uses requestAnimationFrame while scrolling/resizing.
52
+ * If false, uses scroll+resize events directly (still throttled by rAF internally).
53
+ */
54
+ raf?: boolean;
55
+
56
+ /**
57
+ * Debug overlay (default false)
58
+ * Shows progress and a small bar.
59
+ */
60
+ debug?: boolean;
61
+ }
62
+
63
+ function clamp01(n: number) {
64
+ if (n < 0) return 0;
65
+ if (n > 1) return 1;
66
+ return n;
67
+ }
68
+
69
+ function usePrefersReducedMotion(): boolean {
70
+ const [reduced, setReduced] = React.useState(false);
71
+
72
+ React.useEffect(() => {
73
+ if (typeof window === "undefined") return;
74
+ const media = window.matchMedia("(prefers-reduced-motion: reduce)");
75
+ const onChange = () => setReduced(media.matches);
76
+ onChange();
77
+ media.addEventListener?.("change", onChange);
78
+ return () => media.removeEventListener?.("change", onChange);
79
+ }, []);
80
+
81
+ return reduced;
82
+ }
83
+
84
+ /**
85
+ * ScrollProgressWrap
86
+ * Exposes element-local scroll progress (0..1) for timelines, progress bars, parallax, etc.
87
+ */
88
+ export function ScrollProgressWrap({
89
+ axis = "y",
90
+ mode = "cover",
91
+ clamp = true,
92
+ onProgress,
93
+ viewportOffset = 0,
94
+ raf = true,
95
+ debug = false,
96
+ className,
97
+ style,
98
+ children,
99
+ ...props
100
+ }: ScrollProgressWrapProps): React.JSX.Element {
101
+ const ref = React.useRef<HTMLDivElement | null>(null);
102
+ const reducedMotion = usePrefersReducedMotion();
103
+
104
+ const [state, setState] = React.useState<ScrollProgressState>({
105
+ progress: 0,
106
+ rawProgress: 0,
107
+ inView: false,
108
+ rect: null,
109
+ });
110
+
111
+ const last = React.useRef({
112
+ progress: Number.NaN,
113
+ rawProgress: Number.NaN,
114
+ inView: false,
115
+ });
116
+
117
+ const compute = React.useCallback(() => {
118
+ const el = ref.current;
119
+ if (!el) return;
120
+
121
+ const rect = el.getBoundingClientRect();
122
+
123
+ // viewport size with optional offset
124
+ const vw = window.innerWidth + viewportOffset * 2;
125
+ const vh = window.innerHeight + viewportOffset * 2;
126
+
127
+ // shift coordinates if viewportOffset is used (simulate bigger viewport)
128
+ const top = rect.top - viewportOffset;
129
+ const left = rect.left - viewportOffset;
130
+
131
+ const size = axis === "y" ? rect.height : rect.width;
132
+ const viewportSize = axis === "y" ? vh : vw;
133
+ const start = axis === "y" ? top : left;
134
+ const end = start + size;
135
+
136
+ const inView = end > 0 && start < viewportSize;
137
+
138
+ let rawProgress: number;
139
+
140
+ if (mode === "cover") {
141
+ // progress from "just entering" (end=0) to "just leaving" (start=viewport)
142
+ // When element end touches 0 => progress 0
143
+ // When element start touches viewport => progress 1
144
+ const denom = size + viewportSize || 1;
145
+ rawProgress = (viewportSize - start) / denom;
146
+ } else {
147
+ // "contain": from top hits viewport top to bottom hits viewport bottom
148
+ // top=0 => 0, bottom=viewport => 1
149
+ const denom = Math.max(1, viewportSize - size);
150
+ rawProgress = (-start) / denom;
151
+ }
152
+
153
+ const progress = clamp ? clamp01(rawProgress) : rawProgress;
154
+
155
+ // avoid excessive re-renders
156
+ const changed =
157
+ progress !== last.current.progress ||
158
+ rawProgress !== last.current.rawProgress ||
159
+ inView !== last.current.inView;
160
+
161
+ if (!changed) return;
162
+
163
+ last.current = { progress, rawProgress, inView };
164
+
165
+ const next: ScrollProgressState = {
166
+ progress,
167
+ rawProgress,
168
+ inView,
169
+ rect,
170
+ };
171
+
172
+ setState(next);
173
+ onProgress?.(progress, next);
174
+ }, [axis, mode, clamp, onProgress, viewportOffset]);
175
+
176
+ React.useEffect(() => {
177
+ if (typeof window === "undefined") return;
178
+
179
+ // Reduced motion doesn't mean "no updates", but we can still compute normally.
180
+ // Keeping this here in case you want to short-circuit later:
181
+ // if (reducedMotion) return;
182
+
183
+ let ticking = false;
184
+
185
+ const schedule = () => {
186
+ if (ticking) return;
187
+ ticking = true;
188
+ requestAnimationFrame(() => {
189
+ ticking = false;
190
+ compute();
191
+ });
192
+ };
193
+
194
+ // initial
195
+ schedule();
196
+
197
+ const onScroll = () => schedule();
198
+ const onResize = () => schedule();
199
+
200
+ window.addEventListener("scroll", onScroll, { passive: true });
201
+ window.addEventListener("resize", onResize);
202
+
203
+ // If raf=true, we also keep things smooth during momentum scrolling on some devices
204
+ // by scheduling repeatedly while the page is scrolling.
205
+ // We still only update when events fire, but rAF ensures we render at the right time.
206
+ // (If you want "always-on" tracking, we can add an optional loop mode later.)
207
+ return () => {
208
+ window.removeEventListener("scroll", onScroll);
209
+ window.removeEventListener("resize", onResize);
210
+ };
211
+ }, [compute, reducedMotion, raf]);
212
+
213
+ return (
214
+ <div
215
+ ref={ref}
216
+ {...props}
217
+ className={cn("relative", className)}
218
+ style={style}
219
+ >
220
+ {children(state)}
221
+
222
+ {debug ? (
223
+ <div className="pointer-events-none absolute right-3 top-3 z-10 w-36 rounded-xl border border-border/60 bg-background/80 p-3 text-[11px] text-muted-foreground shadow-sm backdrop-blur">
224
+ <div className="flex items-center justify-between">
225
+ <span>progress</span>
226
+ <span className="font-medium text-foreground">
227
+ {Math.round(state.progress * 100)}%
228
+ </span>
229
+ </div>
230
+ <div className="mt-2 h-2 w-full overflow-hidden rounded-full bg-muted">
231
+ <div
232
+ className="h-full bg-foreground"
233
+ style={{ width: `${clamp01(state.progress) * 100}%` }}
234
+ />
235
+ </div>
236
+ <div className="mt-2 flex items-center justify-between">
237
+ <span>inView</span>
238
+ <span className="font-medium text-foreground">
239
+ {state.inView ? "true" : "false"}
240
+ </span>
241
+ </div>
242
+ </div>
243
+ ) : null}
244
+ </div>
245
+ );
246
+ }
247
+
248
+ ScrollProgressWrap.displayName = "ScrollProgressWrap";
@@ -0,0 +1,136 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ type RevealDirection = "up" | "down" | "left" | "right" | "none"
7
+
8
+ export interface RevealWrapProps
9
+ extends React.HTMLAttributes<HTMLDivElement> {
10
+ /** Reveal only once (default true) */
11
+ once?: boolean
12
+
13
+ /** IntersectionObserver threshold (default 0.2) */
14
+ threshold?: number
15
+
16
+ /** IntersectionObserver rootMargin (default "0px 0px -10% 0px") */
17
+ rootMargin?: string
18
+
19
+ /** Transition delay in ms (default 0) */
20
+ delay?: number
21
+
22
+ /** Transition duration in ms (default 600) */
23
+ duration?: number
24
+
25
+ /** Direction to reveal from (default "up") */
26
+ direction?: RevealDirection
27
+
28
+ /** Offset distance in px (default 18) */
29
+ distance?: number
30
+ }
31
+
32
+ function usePrefersReducedMotion(): boolean {
33
+ const [reduced, setReduced] = React.useState(false)
34
+
35
+ React.useEffect(() => {
36
+ if (typeof window === "undefined") return
37
+ const media = window.matchMedia("(prefers-reduced-motion: reduce)")
38
+ const onChange = () => setReduced(media.matches)
39
+ onChange()
40
+ media.addEventListener?.("change", onChange)
41
+ return () => media.removeEventListener?.("change", onChange)
42
+ }, [])
43
+
44
+ return reduced
45
+ }
46
+
47
+ export function RevealWrap({
48
+ once = true,
49
+ threshold = 0.2,
50
+ rootMargin = "0px 0px -10% 0px",
51
+ delay = 0,
52
+ duration = 600,
53
+ direction = "up",
54
+ distance = 18,
55
+ className,
56
+ style,
57
+ children,
58
+ ...props
59
+ }: RevealWrapProps): React.JSX.Element {
60
+ const ref = React.useRef<HTMLDivElement | null>(null)
61
+ const [visible, setVisible] = React.useState(false)
62
+ const reducedMotion = usePrefersReducedMotion()
63
+
64
+ React.useEffect(() => {
65
+ if (reducedMotion) {
66
+ setVisible(true)
67
+ return
68
+ }
69
+
70
+ const el = ref.current
71
+ if (!el) return
72
+
73
+ if (typeof IntersectionObserver === "undefined") {
74
+ setVisible(true)
75
+ return
76
+ }
77
+
78
+ const observer = new IntersectionObserver(
79
+ (entries) => {
80
+ const entry = entries[0]
81
+ if (!entry) return
82
+
83
+ if (entry.isIntersecting) {
84
+ setVisible(true)
85
+ if (once) observer.unobserve(entry.target)
86
+ } else if (!once) {
87
+ setVisible(false)
88
+ }
89
+ },
90
+ { threshold, rootMargin }
91
+ )
92
+
93
+ observer.observe(el)
94
+ return () => observer.disconnect()
95
+ }, [once, threshold, rootMargin, reducedMotion])
96
+
97
+ const clampedDistance = Math.max(0, distance)
98
+ const clampedDuration = Math.max(0, duration)
99
+ const clampedDelay = Math.max(0, delay)
100
+
101
+ const offsetClass =
102
+ direction === "up"
103
+ ? "translate-y-[var(--reveal-distance)]"
104
+ : direction === "down"
105
+ ? "-translate-y-[var(--reveal-distance)]"
106
+ : direction === "left"
107
+ ? "translate-x-[var(--reveal-distance)]"
108
+ : direction === "right"
109
+ ? "-translate-x-[var(--reveal-distance)]"
110
+ : ""
111
+
112
+ return (
113
+ <div
114
+ ref={ref}
115
+ {...props}
116
+ className={cn(
117
+ "transition-[opacity,transform] ease-out will-change-[opacity,transform]",
118
+ "motion-reduce:transition-none motion-reduce:transform-none",
119
+ visible
120
+ ? "opacity-100 translate-x-0 translate-y-0"
121
+ : cn("opacity-0", offsetClass),
122
+ className
123
+ )}
124
+ style={{
125
+ "--reveal-distance": `${clampedDistance}px`,
126
+ transitionDuration: `${clampedDuration}ms`,
127
+ transitionDelay: `${clampedDelay}ms`,
128
+ ...style,
129
+ } as React.CSSProperties}
130
+ >
131
+ {children}
132
+ </div>
133
+ )
134
+ }
135
+
136
+ RevealWrap.displayName = "RevealWrap"