trix-ui 0.2.0 → 0.2.2
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 +110 -19
- package/dist/commands/add/__tests__/add.test.js +18 -6
- package/dist/commands/add/__tests__/add.test.js.map +1 -1
- package/dist/commands/add/analysis.js +6 -1
- package/dist/commands/add/analysis.js.map +1 -1
- package/dist/commands/add/command.js +6 -0
- package/dist/commands/add/command.js.map +1 -1
- package/dist/commands/add/types.d.ts +1 -0
- package/dist/commands/add/ui.js +4 -0
- package/dist/commands/add/ui.js.map +1 -1
- package/dist/commands/add-composite.d.ts +2 -0
- package/dist/commands/add-composite.js +202 -0
- package/dist/commands/add-composite.js.map +1 -0
- package/dist/commands/add-section.js +6 -0
- package/dist/commands/add-section.js.map +1 -1
- package/dist/commands/add-wrapper.js +7 -1
- package/dist/commands/add-wrapper.js.map +1 -1
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init/config.js +1 -0
- package/dist/commands/init/config.js.map +1 -1
- package/dist/commands/list-sections.js +2 -7
- package/dist/commands/list-sections.js.map +1 -1
- package/dist/commands/list-wrappers.js +2 -7
- package/dist/commands/list-wrappers.js.map +1 -1
- package/dist/commands/list.js +51 -8
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/remove-section.js +9 -34
- package/dist/commands/remove-section.js.map +1 -1
- package/dist/commands/remove-wrapper.js +9 -34
- package/dist/commands/remove-wrapper.js.map +1 -1
- package/dist/commands/remove.js +71 -38
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/shared/add-collection.d.ts +2 -1
- package/dist/commands/shared/add-collection.js +10 -13
- package/dist/commands/shared/add-collection.js.map +1 -1
- package/dist/commands/shared/list-entries.d.ts +6 -0
- package/dist/commands/shared/list-entries.js +13 -0
- package/dist/commands/shared/list-entries.js.map +1 -0
- package/dist/commands/shared/name-utils.d.ts +1 -0
- package/dist/commands/shared/name-utils.js +14 -0
- package/dist/commands/shared/name-utils.js.map +1 -0
- package/dist/commands/shared/remove-entries.d.ts +16 -0
- package/dist/commands/shared/remove-entries.js +42 -0
- package/dist/commands/shared/remove-entries.js.map +1 -0
- package/dist/index.js +11 -8
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +1 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/install.js +14 -7
- package/dist/lib/install.js.map +1 -1
- package/dist/lib/lockfile.d.ts +6 -5
- package/dist/lib/lockfile.js +26 -7
- package/dist/lib/lockfile.js.map +1 -1
- package/dist/lib/paths.d.ts +1 -0
- package/dist/lib/paths.js +1 -0
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/registry.d.ts +3 -0
- package/dist/lib/registry.js +15 -0
- package/dist/lib/registry.js.map +1 -1
- package/package.json +12 -12
- package/registry/index.json +67 -128
- package/templates/components/ui/avatar.tsx +109 -0
- package/templates/components/ui/button.tsx +48 -44
- package/templates/components/ui/label.tsx +24 -0
- package/templates/composites/feature-collection-card.tsx +113 -0
- package/templates/composites/music-player-card.tsx +221 -0
- package/templates/composites/user-profile-card.tsx +145 -0
- package/templates/sections/modern-hero.tsx +1226 -0
- package/templates/wrappers/Interative-wrapper.tsx +555 -0
- package/LICENSE.md +0 -21
- package/templates/components/ui/checkbox.tsx +0 -33
- package/templates/components/ui/dialog.tsx +0 -92
- package/templates/components/ui/dropdown.tsx +0 -75
- package/templates/components/ui/select.tsx +0 -24
- package/templates/components/ui/switch.tsx +0 -27
- package/templates/components/ui/toast.tsx +0 -100
- package/templates/sections/cta.tsx +0 -22
- package/templates/sections/feature-grid.tsx +0 -62
- package/templates/sections/hero.tsx +0 -63
- package/templates/wrappers/border-wrapper.tsx +0 -34
- package/templates/wrappers/glow-wrapper.tsx +0 -31
- package/templates/wrappers/lift-wrapper.tsx +0 -27
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
type AnyElement = HTMLElement;
|
|
8
|
+
|
|
9
|
+
function composeRefs<T>(...refs: Array<React.Ref<T> | undefined>) {
|
|
10
|
+
return (node: T) => {
|
|
11
|
+
for (const ref of refs) {
|
|
12
|
+
if (!ref) continue;
|
|
13
|
+
if (typeof ref === "function") ref(node);
|
|
14
|
+
else (ref as React.MutableRefObject<T | null>).current = node;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function usePrefersReducedMotion() {
|
|
20
|
+
const [reduced, setReduced] = React.useState(false);
|
|
21
|
+
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
if (typeof window === "undefined") return;
|
|
24
|
+
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
25
|
+
const onChange = () => setReduced(media.matches);
|
|
26
|
+
onChange();
|
|
27
|
+
media.addEventListener?.("change", onChange);
|
|
28
|
+
return () => media.removeEventListener?.("change", onChange);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
return reduced;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const clamp = (v: number, min: number, max: number) =>
|
|
35
|
+
Math.max(min, Math.min(max, v));
|
|
36
|
+
|
|
37
|
+
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
|
|
38
|
+
|
|
39
|
+
export type InteractiveWrapProps = {
|
|
40
|
+
asChild?: boolean;
|
|
41
|
+
|
|
42
|
+
/** Disable all interactions */
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
|
|
45
|
+
/** Press animation (scale) */
|
|
46
|
+
press?: boolean;
|
|
47
|
+
|
|
48
|
+
/** Cursor-follow glow */
|
|
49
|
+
hoverGlow?: boolean;
|
|
50
|
+
|
|
51
|
+
/** Magnetic hover pull */
|
|
52
|
+
magnetic?: boolean;
|
|
53
|
+
|
|
54
|
+
/** Focus ring when any child inside gets focus */
|
|
55
|
+
focusRing?: boolean;
|
|
56
|
+
|
|
57
|
+
/** Clip glow + children to wrapper bounds */
|
|
58
|
+
clip?: boolean;
|
|
59
|
+
|
|
60
|
+
/** Press scale (default 0.98) */
|
|
61
|
+
pressScale?: number;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Magnetic strength (0 to 1)
|
|
65
|
+
* Recommended range: 0.15 to 0.45
|
|
66
|
+
*/
|
|
67
|
+
magneticStrength?: number;
|
|
68
|
+
|
|
69
|
+
/** Max translate in px when magnetic is enabled (default 14) */
|
|
70
|
+
magneticMaxTranslate?: number;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Smoothing factor for magnetic movement (0..1)
|
|
74
|
+
* Higher = snappier, lower = smoother. Default 0.18
|
|
75
|
+
*/
|
|
76
|
+
magneticDamping?: number;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Glow radius in px
|
|
80
|
+
* Recommended: 180 - 320
|
|
81
|
+
*/
|
|
82
|
+
glowSize?: number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Glow opacity (0 to 1)
|
|
86
|
+
* Recommended: 0.25 - 0.65
|
|
87
|
+
*/
|
|
88
|
+
glowOpacity?: number;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Glow color (solid color is best)
|
|
92
|
+
* Example: "rgba(124, 58, 237, 1)"
|
|
93
|
+
*/
|
|
94
|
+
glowColor?: string;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Smoothness for glow follow (0..1)
|
|
98
|
+
* Higher = snappier, lower = smoother. Default 0.28
|
|
99
|
+
*/
|
|
100
|
+
glowDamping?: number;
|
|
101
|
+
|
|
102
|
+
/** Blend mode for glow layer (default "screen") */
|
|
103
|
+
glowBlendMode?: React.CSSProperties["mixBlendMode"];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Optional: wrapper-level press handler (pointer + keyboard).
|
|
107
|
+
* If provided (and not asChild), wrapper becomes button-like (role/tabIndex).
|
|
108
|
+
*/
|
|
109
|
+
onPress?: (
|
|
110
|
+
e: React.PointerEvent<AnyElement> | React.KeyboardEvent<AnyElement>
|
|
111
|
+
) => void;
|
|
112
|
+
|
|
113
|
+
className?: string;
|
|
114
|
+
children: React.ReactNode;
|
|
115
|
+
} & React.HTMLAttributes<AnyElement>;
|
|
116
|
+
|
|
117
|
+
export const InteractiveWrap = React.forwardRef<AnyElement, InteractiveWrapProps>(
|
|
118
|
+
(
|
|
119
|
+
{
|
|
120
|
+
asChild,
|
|
121
|
+
disabled = false,
|
|
122
|
+
|
|
123
|
+
press = true,
|
|
124
|
+
hoverGlow = true,
|
|
125
|
+
magnetic = false,
|
|
126
|
+
focusRing = true,
|
|
127
|
+
clip = true,
|
|
128
|
+
|
|
129
|
+
pressScale = 0.98,
|
|
130
|
+
|
|
131
|
+
magneticStrength = 0.32,
|
|
132
|
+
magneticMaxTranslate = 14,
|
|
133
|
+
magneticDamping = 0.18,
|
|
134
|
+
|
|
135
|
+
glowSize = 240,
|
|
136
|
+
glowOpacity = 0.45,
|
|
137
|
+
glowColor = "rgba(124, 58, 237, 1)",
|
|
138
|
+
glowDamping = 0.28,
|
|
139
|
+
glowBlendMode = "screen",
|
|
140
|
+
|
|
141
|
+
onPress,
|
|
142
|
+
|
|
143
|
+
className,
|
|
144
|
+
children,
|
|
145
|
+
style,
|
|
146
|
+
...props
|
|
147
|
+
},
|
|
148
|
+
forwardedRef
|
|
149
|
+
) => {
|
|
150
|
+
const Comp = asChild ? Slot : "div";
|
|
151
|
+
|
|
152
|
+
const internalRef = React.useRef<AnyElement | null>(null);
|
|
153
|
+
const ref = composeRefs(forwardedRef, internalRef);
|
|
154
|
+
|
|
155
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
156
|
+
const canAnimate = !disabled && !reducedMotion;
|
|
157
|
+
|
|
158
|
+
const [hovered, setHovered] = React.useState(false);
|
|
159
|
+
const [pressed, setPressed] = React.useState(false);
|
|
160
|
+
|
|
161
|
+
// RAF loop: keeps magnet + glow stable and avoids CSS-transition lag.
|
|
162
|
+
const rafRef = React.useRef<number | null>(null);
|
|
163
|
+
const runningRef = React.useRef(false);
|
|
164
|
+
|
|
165
|
+
const pointerDownRef = React.useRef(false);
|
|
166
|
+
const keyDownRef = React.useRef(false);
|
|
167
|
+
|
|
168
|
+
const stateRef = React.useRef({
|
|
169
|
+
// current
|
|
170
|
+
x: 0,
|
|
171
|
+
y: 0,
|
|
172
|
+
tx: 0,
|
|
173
|
+
ty: 0,
|
|
174
|
+
scale: 1,
|
|
175
|
+
|
|
176
|
+
// targets
|
|
177
|
+
targetX: 0,
|
|
178
|
+
targetY: 0,
|
|
179
|
+
targetTx: 0,
|
|
180
|
+
targetTy: 0,
|
|
181
|
+
targetScale: 1,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const applyVars = React.useCallback(() => {
|
|
185
|
+
const el = internalRef.current;
|
|
186
|
+
if (!el) return;
|
|
187
|
+
|
|
188
|
+
const s = stateRef.current;
|
|
189
|
+
|
|
190
|
+
el.style.setProperty("--iw-x", `${s.x}px`);
|
|
191
|
+
el.style.setProperty("--iw-y", `${s.y}px`);
|
|
192
|
+
el.style.setProperty("--iw-tx", `${s.tx}px`);
|
|
193
|
+
el.style.setProperty("--iw-ty", `${s.ty}px`);
|
|
194
|
+
el.style.setProperty("--iw-scale", `${s.scale}`);
|
|
195
|
+
}, []);
|
|
196
|
+
|
|
197
|
+
const startLoop = React.useCallback(() => {
|
|
198
|
+
if (!canAnimate) return;
|
|
199
|
+
if (runningRef.current) return;
|
|
200
|
+
runningRef.current = true;
|
|
201
|
+
|
|
202
|
+
const tick = () => {
|
|
203
|
+
const el = internalRef.current;
|
|
204
|
+
if (!el) {
|
|
205
|
+
runningRef.current = false;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const s = stateRef.current;
|
|
210
|
+
|
|
211
|
+
// smooth follow
|
|
212
|
+
s.x = lerp(s.x, s.targetX, clamp(glowDamping, 0.05, 0.75));
|
|
213
|
+
s.y = lerp(s.y, s.targetY, clamp(glowDamping, 0.05, 0.75));
|
|
214
|
+
|
|
215
|
+
s.tx = lerp(s.tx, s.targetTx, clamp(magneticDamping, 0.06, 0.6));
|
|
216
|
+
s.ty = lerp(s.ty, s.targetTy, clamp(magneticDamping, 0.06, 0.6));
|
|
217
|
+
|
|
218
|
+
// scale can be a bit snappier
|
|
219
|
+
s.scale = lerp(s.scale, s.targetScale, 0.22);
|
|
220
|
+
|
|
221
|
+
applyVars();
|
|
222
|
+
|
|
223
|
+
const near = (a: number, b: number) => Math.abs(a - b) < 0.12;
|
|
224
|
+
|
|
225
|
+
const settled =
|
|
226
|
+
near(s.x, s.targetX) &&
|
|
227
|
+
near(s.y, s.targetY) &&
|
|
228
|
+
near(s.tx, s.targetTx) &&
|
|
229
|
+
near(s.ty, s.targetTy) &&
|
|
230
|
+
Math.abs(s.scale - s.targetScale) < 0.01;
|
|
231
|
+
|
|
232
|
+
// keep running while hovered/pressed, or while returning to rest
|
|
233
|
+
const keepAlive = hovered || pressed || !settled;
|
|
234
|
+
|
|
235
|
+
if (keepAlive) {
|
|
236
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
237
|
+
} else {
|
|
238
|
+
runningRef.current = false;
|
|
239
|
+
rafRef.current = null;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
244
|
+
}, [
|
|
245
|
+
canAnimate,
|
|
246
|
+
applyVars,
|
|
247
|
+
glowDamping,
|
|
248
|
+
magneticDamping,
|
|
249
|
+
hovered,
|
|
250
|
+
pressed,
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
const setCenterTargets = React.useCallback(() => {
|
|
254
|
+
const el = internalRef.current;
|
|
255
|
+
if (!el) return;
|
|
256
|
+
const rect = el.getBoundingClientRect();
|
|
257
|
+
const cx = rect.width / 2;
|
|
258
|
+
const cy = rect.height / 2;
|
|
259
|
+
const s = stateRef.current;
|
|
260
|
+
s.targetX = cx;
|
|
261
|
+
s.targetY = cy;
|
|
262
|
+
|
|
263
|
+
// also initialize current to avoid jump on first hover
|
|
264
|
+
if (!hovered) {
|
|
265
|
+
s.x = cx;
|
|
266
|
+
s.y = cy;
|
|
267
|
+
}
|
|
268
|
+
}, [hovered]);
|
|
269
|
+
|
|
270
|
+
React.useEffect(() => {
|
|
271
|
+
// init at center so glow looks correct before mouse moves
|
|
272
|
+
setCenterTargets();
|
|
273
|
+
applyVars();
|
|
274
|
+
|
|
275
|
+
const el = internalRef.current;
|
|
276
|
+
if (!el) return;
|
|
277
|
+
|
|
278
|
+
const ro = new ResizeObserver(() => {
|
|
279
|
+
setCenterTargets();
|
|
280
|
+
startLoop();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
ro.observe(el);
|
|
284
|
+
|
|
285
|
+
return () => {
|
|
286
|
+
ro.disconnect();
|
|
287
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
288
|
+
rafRef.current = null;
|
|
289
|
+
runningRef.current = false;
|
|
290
|
+
};
|
|
291
|
+
}, [applyVars, setCenterTargets, startLoop]);
|
|
292
|
+
|
|
293
|
+
const setMagnetTargets = React.useCallback(
|
|
294
|
+
(clientX: number, clientY: number, pointerType?: string) => {
|
|
295
|
+
const el = internalRef.current;
|
|
296
|
+
if (!el) return;
|
|
297
|
+
|
|
298
|
+
const rect = el.getBoundingClientRect();
|
|
299
|
+
|
|
300
|
+
// local coords for glow
|
|
301
|
+
const x = clientX - rect.left;
|
|
302
|
+
const y = clientY - rect.top;
|
|
303
|
+
|
|
304
|
+
const s = stateRef.current;
|
|
305
|
+
s.targetX = x;
|
|
306
|
+
s.targetY = y;
|
|
307
|
+
|
|
308
|
+
const isTouch = pointerType === "touch";
|
|
309
|
+
const allowMouseFX = !isTouch;
|
|
310
|
+
|
|
311
|
+
if (canAnimate && magnetic && allowMouseFX) {
|
|
312
|
+
const dx = x - rect.width / 2;
|
|
313
|
+
const dy = y - rect.height / 2;
|
|
314
|
+
|
|
315
|
+
const max = Math.min(rect.width, rect.height) / 2;
|
|
316
|
+
const nx = max > 0 ? dx / max : 0;
|
|
317
|
+
const ny = max > 0 ? dy / max : 0;
|
|
318
|
+
|
|
319
|
+
const cx = clamp(nx, -1, 1);
|
|
320
|
+
const cy = clamp(ny, -1, 1);
|
|
321
|
+
|
|
322
|
+
const translateMax =
|
|
323
|
+
clamp(magneticMaxTranslate, 0, 40) *
|
|
324
|
+
clamp(magneticStrength, 0, 1);
|
|
325
|
+
|
|
326
|
+
s.targetTx = cx * translateMax;
|
|
327
|
+
s.targetTy = cy * translateMax;
|
|
328
|
+
} else {
|
|
329
|
+
s.targetTx = 0;
|
|
330
|
+
s.targetTy = 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
startLoop();
|
|
334
|
+
},
|
|
335
|
+
[
|
|
336
|
+
canAnimate,
|
|
337
|
+
magnetic,
|
|
338
|
+
magneticMaxTranslate,
|
|
339
|
+
magneticStrength,
|
|
340
|
+
startLoop,
|
|
341
|
+
]
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const handlePointerEnter = (e: React.PointerEvent<AnyElement>) => {
|
|
345
|
+
if (disabled) return;
|
|
346
|
+
setHovered(true);
|
|
347
|
+
setCenterTargets();
|
|
348
|
+
// start loop so glow fades in smoothly even before move
|
|
349
|
+
startLoop();
|
|
350
|
+
props.onPointerEnter?.(e);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const handlePointerLeave = (e: React.PointerEvent<AnyElement>) => {
|
|
354
|
+
if (disabled) return;
|
|
355
|
+
|
|
356
|
+
setHovered(false);
|
|
357
|
+
setPressed(false);
|
|
358
|
+
pointerDownRef.current = false;
|
|
359
|
+
|
|
360
|
+
// return to rest smoothly
|
|
361
|
+
const s = stateRef.current;
|
|
362
|
+
s.targetTx = 0;
|
|
363
|
+
s.targetTy = 0;
|
|
364
|
+
s.targetScale = 1;
|
|
365
|
+
setCenterTargets();
|
|
366
|
+
startLoop();
|
|
367
|
+
|
|
368
|
+
props.onPointerLeave?.(e);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const handlePointerMove = (e: React.PointerEvent<AnyElement>) => {
|
|
372
|
+
if (disabled) return;
|
|
373
|
+
setMagnetTargets(e.clientX, e.clientY, e.pointerType);
|
|
374
|
+
props.onPointerMove?.(e);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const handlePointerDown = (e: React.PointerEvent<AnyElement>) => {
|
|
378
|
+
if (disabled) return;
|
|
379
|
+
|
|
380
|
+
// only primary button (left click)
|
|
381
|
+
if ("button" in e && e.button !== 0) {
|
|
382
|
+
props.onPointerDown?.(e);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
pointerDownRef.current = true;
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
(e.currentTarget as AnyElement).setPointerCapture?.(e.pointerId);
|
|
390
|
+
} catch {
|
|
391
|
+
// ignore
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (canAnimate && press) {
|
|
395
|
+
setPressed(true);
|
|
396
|
+
stateRef.current.targetScale = clamp(pressScale, 0.9, 1);
|
|
397
|
+
startLoop();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
props.onPointerDown?.(e);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const handlePointerUp = (e: React.PointerEvent<AnyElement>) => {
|
|
404
|
+
if (disabled) return;
|
|
405
|
+
|
|
406
|
+
if (press) setPressed(false);
|
|
407
|
+
stateRef.current.targetScale = 1;
|
|
408
|
+
startLoop();
|
|
409
|
+
|
|
410
|
+
// wrapper-level click (only if pointer started here)
|
|
411
|
+
if (onPress && pointerDownRef.current) onPress(e);
|
|
412
|
+
pointerDownRef.current = false;
|
|
413
|
+
|
|
414
|
+
props.onPointerUp?.(e);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const handlePointerCancel = (e: React.PointerEvent<AnyElement>) => {
|
|
418
|
+
if (disabled) return;
|
|
419
|
+
setPressed(false);
|
|
420
|
+
pointerDownRef.current = false;
|
|
421
|
+
|
|
422
|
+
const s = stateRef.current;
|
|
423
|
+
s.targetTx = 0;
|
|
424
|
+
s.targetTy = 0;
|
|
425
|
+
s.targetScale = 1;
|
|
426
|
+
startLoop();
|
|
427
|
+
|
|
428
|
+
props.onPointerCancel?.(e);
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const handleKeyDown = (e: React.KeyboardEvent<AnyElement>) => {
|
|
432
|
+
if (disabled) return;
|
|
433
|
+
|
|
434
|
+
// Space/Enter press feedback (role=button behavior)
|
|
435
|
+
if ((e.key === "Enter" || e.key === " ") && press && canAnimate) {
|
|
436
|
+
if (!keyDownRef.current) {
|
|
437
|
+
keyDownRef.current = true;
|
|
438
|
+
setPressed(true);
|
|
439
|
+
stateRef.current.targetScale = clamp(pressScale, 0.9, 1);
|
|
440
|
+
startLoop();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Prevent scroll on Space when wrapper acts like a button
|
|
445
|
+
if ((e.key === " " || e.key === "Enter") && onPress) {
|
|
446
|
+
if (e.key === " ") e.preventDefault();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
props.onKeyDown?.(e);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const handleKeyUp = (e: React.KeyboardEvent<AnyElement>) => {
|
|
453
|
+
if (disabled) return;
|
|
454
|
+
|
|
455
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
456
|
+
if (keyDownRef.current) {
|
|
457
|
+
keyDownRef.current = false;
|
|
458
|
+
setPressed(false);
|
|
459
|
+
stateRef.current.targetScale = 1;
|
|
460
|
+
startLoop();
|
|
461
|
+
if (onPress) onPress(e);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
props.onKeyUp?.(e);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const buttonLikeProps =
|
|
469
|
+
!asChild && onPress
|
|
470
|
+
? ({
|
|
471
|
+
role: "button",
|
|
472
|
+
tabIndex: disabled ? -1 : props.tabIndex ?? 0,
|
|
473
|
+
"aria-disabled": disabled ? true : undefined,
|
|
474
|
+
} as const)
|
|
475
|
+
: ({} as const);
|
|
476
|
+
|
|
477
|
+
const userTransform =
|
|
478
|
+
typeof style?.transform === "string" ? style.transform : "";
|
|
479
|
+
|
|
480
|
+
// If you keep CSS transition on transform, magnetic will feel delayed.
|
|
481
|
+
// We rely on RAF smoothing instead, so no transform transition here.
|
|
482
|
+
const baseTransform = canAnimate
|
|
483
|
+
? `translate3d(var(--iw-tx, 0px), var(--iw-ty, 0px), 0) scale(var(--iw-scale, 1))`
|
|
484
|
+
: "";
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<Comp
|
|
488
|
+
ref={ref}
|
|
489
|
+
{...buttonLikeProps}
|
|
490
|
+
{...props}
|
|
491
|
+
onPointerEnter={handlePointerEnter}
|
|
492
|
+
onPointerLeave={handlePointerLeave}
|
|
493
|
+
onPointerMove={handlePointerMove}
|
|
494
|
+
onPointerDown={handlePointerDown}
|
|
495
|
+
onPointerUp={handlePointerUp}
|
|
496
|
+
onPointerCancel={handlePointerCancel}
|
|
497
|
+
onKeyDown={handleKeyDown}
|
|
498
|
+
onKeyUp={handleKeyUp}
|
|
499
|
+
data-hovered={hovered ? "true" : "false"}
|
|
500
|
+
data-pressed={pressed ? "true" : "false"}
|
|
501
|
+
className={cn(
|
|
502
|
+
"relative isolate",
|
|
503
|
+
clip ? "overflow-hidden" : "overflow-visible",
|
|
504
|
+
canAnimate && "will-change-transform",
|
|
505
|
+
disabled ? "pointer-events-none opacity-60" : "pointer-events-auto",
|
|
506
|
+
focusRing &&
|
|
507
|
+
!disabled &&
|
|
508
|
+
"focus-within:outline-none focus-within:ring-2 focus-within:ring-ring/30 focus-within:ring-offset-2 focus-within:ring-offset-background",
|
|
509
|
+
className
|
|
510
|
+
)}
|
|
511
|
+
style={{
|
|
512
|
+
...style,
|
|
513
|
+
// Better mobile feel when wrapper is clickable
|
|
514
|
+
touchAction: onPress ? "manipulation" : style?.touchAction,
|
|
515
|
+
transform: canAnimate
|
|
516
|
+
? `${baseTransform}${userTransform ? ` ${userTransform}` : ""}`
|
|
517
|
+
: userTransform || style?.transform,
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
520
|
+
{/* Cursor-follow glow layer */}
|
|
521
|
+
{hoverGlow && (
|
|
522
|
+
<div
|
|
523
|
+
aria-hidden="true"
|
|
524
|
+
className={cn(
|
|
525
|
+
"pointer-events-none absolute inset-0",
|
|
526
|
+
"transition-opacity duration-200 ease-out"
|
|
527
|
+
)}
|
|
528
|
+
style={{
|
|
529
|
+
opacity: hovered && !disabled ? glowOpacity : 0,
|
|
530
|
+
mixBlendMode: glowBlendMode,
|
|
531
|
+
background: `radial-gradient(${glowSize}px circle at var(--iw-x, 50%) var(--iw-y, 50%), ${glowColor}, transparent 62%)`,
|
|
532
|
+
}}
|
|
533
|
+
/>
|
|
534
|
+
)}
|
|
535
|
+
|
|
536
|
+
{/* Content */}
|
|
537
|
+
<div className="relative z-[1]">{children}</div>
|
|
538
|
+
</Comp>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
InteractiveWrap.displayName = "InteractiveWrap";
|
|
544
|
+
|
|
545
|
+
/* -------------------------------- Usage ---------------------------------
|
|
546
|
+
|
|
547
|
+
<InteractiveWrap hoverGlow magnetic press className="rounded-2xl">
|
|
548
|
+
<button className="px-6 py-3 rounded-2xl bg-white/10 text-white">Button</button>
|
|
549
|
+
</InteractiveWrap>
|
|
550
|
+
|
|
551
|
+
<InteractiveWrap onPress={() => console.log("pressed")} className="rounded-xl p-3">
|
|
552
|
+
<div>Card-like clickable wrapper</div>
|
|
553
|
+
</InteractiveWrap>
|
|
554
|
+
|
|
555
|
+
--------------------------------------------------------------------------- */
|
package/LICENSE.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2023 shadcn
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
3
|
-
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
5
|
-
|
|
6
|
-
function CheckIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
7
|
-
return (
|
|
8
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" {...props}>
|
|
9
|
-
<polyline points="3.5 8.5 6.5 11.5 12.5 4.5" />
|
|
10
|
-
</svg>
|
|
11
|
-
)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const Checkbox = React.forwardRef<
|
|
15
|
-
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
|
16
|
-
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
|
17
|
-
>(({ className, ...props }, ref) => (
|
|
18
|
-
<CheckboxPrimitive.Root
|
|
19
|
-
ref={ref}
|
|
20
|
-
className={cn(
|
|
21
|
-
"peer h-4 w-4 shrink-0 rounded-sm border border-input bg-background shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
22
|
-
className
|
|
23
|
-
)}
|
|
24
|
-
{...props}
|
|
25
|
-
>
|
|
26
|
-
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
|
|
27
|
-
<CheckIcon className="h-3.5 w-3.5 stroke-[2.5]" />
|
|
28
|
-
</CheckboxPrimitive.Indicator>
|
|
29
|
-
</CheckboxPrimitive.Root>
|
|
30
|
-
))
|
|
31
|
-
Checkbox.displayName = "Checkbox"
|
|
32
|
-
|
|
33
|
-
export { Checkbox }
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
3
|
-
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
5
|
-
|
|
6
|
-
const Dialog = DialogPrimitive.Root
|
|
7
|
-
const DialogTrigger = DialogPrimitive.Trigger
|
|
8
|
-
const DialogClose = DialogPrimitive.Close
|
|
9
|
-
|
|
10
|
-
const DialogOverlay = React.forwardRef<
|
|
11
|
-
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
12
|
-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
13
|
-
>(({ className, ...props }, ref) => (
|
|
14
|
-
<DialogPrimitive.Overlay
|
|
15
|
-
ref={ref}
|
|
16
|
-
className={cn("fixed inset-0 z-50 bg-black/50", className)}
|
|
17
|
-
{...props}
|
|
18
|
-
/>
|
|
19
|
-
))
|
|
20
|
-
DialogOverlay.displayName = "DialogOverlay"
|
|
21
|
-
|
|
22
|
-
const DialogContent = React.forwardRef<
|
|
23
|
-
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
24
|
-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
25
|
-
>(({ className, ...props }, ref) => (
|
|
26
|
-
<DialogPrimitive.Portal>
|
|
27
|
-
<DialogOverlay />
|
|
28
|
-
<DialogPrimitive.Content
|
|
29
|
-
ref={ref}
|
|
30
|
-
className={cn(
|
|
31
|
-
"fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-background p-6 shadow-lg focus-visible:outline-none",
|
|
32
|
-
className
|
|
33
|
-
)}
|
|
34
|
-
{...props}
|
|
35
|
-
/>
|
|
36
|
-
</DialogPrimitive.Portal>
|
|
37
|
-
))
|
|
38
|
-
DialogContent.displayName = "DialogContent"
|
|
39
|
-
|
|
40
|
-
const DialogHeader = ({
|
|
41
|
-
className,
|
|
42
|
-
...props
|
|
43
|
-
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
44
|
-
<div className={cn("flex flex-col space-y-2 text-left", className)} {...props} />
|
|
45
|
-
)
|
|
46
|
-
DialogHeader.displayName = "DialogHeader"
|
|
47
|
-
|
|
48
|
-
const DialogFooter = ({
|
|
49
|
-
className,
|
|
50
|
-
...props
|
|
51
|
-
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
52
|
-
<div
|
|
53
|
-
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
|
54
|
-
{...props}
|
|
55
|
-
/>
|
|
56
|
-
)
|
|
57
|
-
DialogFooter.displayName = "DialogFooter"
|
|
58
|
-
|
|
59
|
-
const DialogTitle = React.forwardRef<
|
|
60
|
-
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
61
|
-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
62
|
-
>(({ className, ...props }, ref) => (
|
|
63
|
-
<DialogPrimitive.Title
|
|
64
|
-
ref={ref}
|
|
65
|
-
className={cn("text-lg font-semibold", className)}
|
|
66
|
-
{...props}
|
|
67
|
-
/>
|
|
68
|
-
))
|
|
69
|
-
DialogTitle.displayName = "DialogTitle"
|
|
70
|
-
|
|
71
|
-
const DialogDescription = React.forwardRef<
|
|
72
|
-
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
73
|
-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
74
|
-
>(({ className, ...props }, ref) => (
|
|
75
|
-
<DialogPrimitive.Description
|
|
76
|
-
ref={ref}
|
|
77
|
-
className={cn("text-sm text-muted-foreground", className)}
|
|
78
|
-
{...props}
|
|
79
|
-
/>
|
|
80
|
-
))
|
|
81
|
-
DialogDescription.displayName = "DialogDescription"
|
|
82
|
-
|
|
83
|
-
export {
|
|
84
|
-
Dialog,
|
|
85
|
-
DialogTrigger,
|
|
86
|
-
DialogClose,
|
|
87
|
-
DialogContent,
|
|
88
|
-
DialogHeader,
|
|
89
|
-
DialogFooter,
|
|
90
|
-
DialogTitle,
|
|
91
|
-
DialogDescription
|
|
92
|
-
}
|