@arolariu/components 0.0.36 → 0.0.38

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 (104) hide show
  1. package/changelog.md +15 -0
  2. package/dist/cjs/components/ui/background-beams.cjs +200 -0
  3. package/dist/cjs/components/ui/background-beams.cjs.map +1 -0
  4. package/dist/cjs/components/ui/bubble-background.cjs +214 -0
  5. package/dist/cjs/components/ui/bubble-background.cjs.map +1 -0
  6. package/dist/cjs/components/ui/chart.cjs.map +1 -1
  7. package/dist/cjs/components/ui/counting-number.cjs +95 -0
  8. package/dist/cjs/components/ui/counting-number.cjs.map +1 -0
  9. package/dist/cjs/components/ui/dot-background.cjs +131 -0
  10. package/dist/cjs/components/ui/dot-background.cjs.map +1 -0
  11. package/dist/cjs/components/ui/drawer.cjs.map +1 -1
  12. package/dist/cjs/components/ui/dropdown-menu.cjs.map +1 -1
  13. package/dist/cjs/components/ui/dropdrawer.cjs +627 -0
  14. package/dist/cjs/components/ui/dropdrawer.cjs.map +1 -0
  15. package/dist/cjs/components/ui/fireworks-background.cjs +259 -0
  16. package/dist/cjs/components/ui/fireworks-background.cjs.map +1 -0
  17. package/dist/cjs/components/ui/flip-button.cjs +100 -0
  18. package/dist/cjs/components/ui/flip-button.cjs.map +1 -0
  19. package/dist/cjs/components/ui/gradient-background.cjs +60 -0
  20. package/dist/cjs/components/ui/gradient-background.cjs.map +1 -0
  21. package/dist/cjs/components/ui/gradient-text.cjs +83 -0
  22. package/dist/cjs/components/ui/gradient-text.cjs.map +1 -0
  23. package/dist/cjs/components/ui/highlight-text.cjs +74 -0
  24. package/dist/cjs/components/ui/highlight-text.cjs.map +1 -0
  25. package/dist/cjs/components/ui/hole-background.cjs +361 -0
  26. package/dist/cjs/components/ui/hole-background.cjs.map +1 -0
  27. package/dist/cjs/components/ui/pagination.cjs.map +1 -1
  28. package/dist/cjs/components/ui/ripple-button.cjs +108 -0
  29. package/dist/cjs/components/ui/ripple-button.cjs.map +1 -0
  30. package/dist/cjs/components/ui/scratcher.cjs +179 -0
  31. package/dist/cjs/components/ui/scratcher.cjs.map +1 -0
  32. package/dist/cjs/hooks/use-mobile.cjs.map +1 -1
  33. package/dist/cjs/index.cjs +86 -4
  34. package/dist/cjs/index.css +2859 -160
  35. package/dist/esm/components/ui/background-beams.js +166 -0
  36. package/dist/esm/components/ui/background-beams.js.map +1 -0
  37. package/dist/esm/components/ui/bubble-background.js +180 -0
  38. package/dist/esm/components/ui/bubble-background.js.map +1 -0
  39. package/dist/esm/components/ui/chart.js.map +1 -1
  40. package/dist/esm/components/ui/counting-number.js +61 -0
  41. package/dist/esm/components/ui/counting-number.js.map +1 -0
  42. package/dist/esm/components/ui/dot-background.js +97 -0
  43. package/dist/esm/components/ui/dot-background.js.map +1 -0
  44. package/dist/esm/components/ui/drawer.js.map +1 -1
  45. package/dist/esm/components/ui/dropdown-menu.js.map +1 -1
  46. package/dist/esm/components/ui/dropdrawer.js +563 -0
  47. package/dist/esm/components/ui/dropdrawer.js.map +1 -0
  48. package/dist/esm/components/ui/fireworks-background.js +225 -0
  49. package/dist/esm/components/ui/fireworks-background.js.map +1 -0
  50. package/dist/esm/components/ui/flip-button.js +66 -0
  51. package/dist/esm/components/ui/flip-button.js.map +1 -0
  52. package/dist/esm/components/ui/gradient-background.js +26 -0
  53. package/dist/esm/components/ui/gradient-background.js.map +1 -0
  54. package/dist/esm/components/ui/gradient-text.js +49 -0
  55. package/dist/esm/components/ui/gradient-text.js.map +1 -0
  56. package/dist/esm/components/ui/highlight-text.js +40 -0
  57. package/dist/esm/components/ui/highlight-text.js.map +1 -0
  58. package/dist/esm/components/ui/hole-background.js +327 -0
  59. package/dist/esm/components/ui/hole-background.js.map +1 -0
  60. package/dist/esm/components/ui/pagination.js.map +1 -1
  61. package/dist/esm/components/ui/ripple-button.js +74 -0
  62. package/dist/esm/components/ui/ripple-button.js.map +1 -0
  63. package/dist/esm/components/ui/scratcher.js +145 -0
  64. package/dist/esm/components/ui/scratcher.js.map +1 -0
  65. package/dist/esm/hooks/use-mobile.js.map +1 -1
  66. package/dist/esm/index.css +2859 -160
  67. package/dist/esm/index.js +37 -1
  68. package/dist/index.css +2859 -160
  69. package/dist/index.js +37 -1
  70. package/dist/types/components/ui/background-beams.d.ts +4 -0
  71. package/dist/types/components/ui/bubble-background.d.ts +16 -0
  72. package/dist/types/components/ui/counting-number.d.ts +15 -0
  73. package/dist/types/components/ui/dot-background.d.ts +57 -0
  74. package/dist/types/components/ui/dropdrawer.d.ts +23 -0
  75. package/dist/types/components/ui/fireworks-background.d.ts +24 -0
  76. package/dist/types/components/ui/flip-button.d.ts +13 -0
  77. package/dist/types/components/ui/gradient-background.d.ts +7 -0
  78. package/dist/types/components/ui/gradient-text.d.ts +10 -0
  79. package/dist/types/components/ui/highlight-text.d.ts +11 -0
  80. package/dist/types/components/ui/hole-background.d.ts +9 -0
  81. package/dist/types/components/ui/ripple-button.d.ts +10 -0
  82. package/dist/types/components/ui/scratcher.d.ts +12 -0
  83. package/dist/types/hooks/use-mobile.d.ts +23 -0
  84. package/dist/types/index.d.ts +13 -0
  85. package/package.json +159 -70
  86. package/readme.md +12 -7
  87. package/src/components/ui/background-beams.tsx +142 -0
  88. package/src/components/ui/bubble-background.tsx +187 -0
  89. package/src/components/ui/counting-number.tsx +108 -0
  90. package/src/components/ui/dot-background.tsx +158 -0
  91. package/src/components/ui/drawer.tsx +4 -4
  92. package/src/components/ui/dropdown-menu.tsx +9 -9
  93. package/src/components/ui/dropdrawer.tsx +973 -0
  94. package/src/components/ui/fireworks-background.tsx +378 -0
  95. package/src/components/ui/flip-button.tsx +110 -0
  96. package/src/components/ui/gradient-background.tsx +43 -0
  97. package/src/components/ui/gradient-text.tsx +65 -0
  98. package/src/components/ui/highlight-text.tsx +71 -0
  99. package/src/components/ui/hole-background.tsx +361 -0
  100. package/src/components/ui/ripple-button.tsx +111 -0
  101. package/src/components/ui/scratcher.tsx +171 -0
  102. package/src/hooks/use-mobile.tsx +37 -12
  103. package/src/index.ts +69 -0
  104. package/tsconfig.json +50 -50
@@ -0,0 +1,378 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const rand = (min: number, max: number): number =>
8
+ Math.random() * (max - min) + min;
9
+ const randInt = (min: number, max: number): number =>
10
+ Math.floor(Math.random() * (max - min) + min);
11
+ const randColor = (): string => `hsl(${randInt(0, 360)}, 100%, 50%)`;
12
+
13
+ interface ParticleType {
14
+ x: number;
15
+ y: number;
16
+ color: string;
17
+ speed: number;
18
+ direction: number;
19
+ vx: number;
20
+ vy: number;
21
+ gravity: number;
22
+ friction: number;
23
+ alpha: number;
24
+ decay: number;
25
+ size: number;
26
+ update: () => void;
27
+ draw: (ctx: CanvasRenderingContext2D) => void;
28
+ isAlive: () => boolean;
29
+ }
30
+
31
+ const createParticle = (
32
+ x: number,
33
+ y: number,
34
+ color: string,
35
+ speed: number,
36
+ direction: number,
37
+ gravity: number,
38
+ friction: number,
39
+ size: number
40
+ ): ParticleType => {
41
+ const vx = Math.cos(direction) * speed;
42
+ const vy = Math.sin(direction) * speed;
43
+ const alpha = 1;
44
+ const decay = rand(0.005, 0.02);
45
+
46
+ return {
47
+ x,
48
+ y,
49
+ color,
50
+ speed,
51
+ direction,
52
+ vx,
53
+ vy,
54
+ gravity,
55
+ friction,
56
+ alpha,
57
+ decay,
58
+ size,
59
+ update() {
60
+ this.vx *= this.friction;
61
+ this.vy *= this.friction;
62
+ this.vy += this.gravity;
63
+ this.x += this.vx;
64
+ this.y += this.vy;
65
+ this.alpha -= this.decay;
66
+ },
67
+ draw(ctx: CanvasRenderingContext2D) {
68
+ ctx.save();
69
+ ctx.globalAlpha = this.alpha;
70
+ ctx.beginPath();
71
+ ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
72
+ ctx.fillStyle = this.color;
73
+ ctx.fill();
74
+ ctx.restore();
75
+ },
76
+ isAlive() {
77
+ return this.alpha > 0;
78
+ },
79
+ };
80
+ };
81
+
82
+ interface FireworkType {
83
+ x: number;
84
+ y: number;
85
+ targetY: number;
86
+ color: string;
87
+ speed: number;
88
+ size: number;
89
+ angle: number;
90
+ vx: number;
91
+ vy: number;
92
+ trail: { x: number; y: number }[];
93
+ trailLength: number;
94
+ exploded: boolean;
95
+ update: () => boolean;
96
+ explode: () => void;
97
+ draw: (ctx: CanvasRenderingContext2D) => void;
98
+ }
99
+
100
+ const createFirework = (
101
+ x: number,
102
+ y: number,
103
+ targetY: number,
104
+ color: string,
105
+ speed: number,
106
+ size: number,
107
+ particleSpeed: { min: number; max: number } | number,
108
+ particleSize: { min: number; max: number } | number,
109
+ onExplode: (particles: ParticleType[]) => void
110
+ ): FireworkType => {
111
+ const angle = -Math.PI / 2 + rand(-0.3, 0.3);
112
+ const vx = Math.cos(angle) * speed;
113
+ const vy = Math.sin(angle) * speed;
114
+ const trail: { x: number; y: number }[] = [];
115
+ const trailLength = randInt(10, 25);
116
+
117
+ return {
118
+ x,
119
+ y,
120
+ targetY,
121
+ color,
122
+ speed,
123
+ size,
124
+ angle,
125
+ vx,
126
+ vy,
127
+ trail,
128
+ trailLength,
129
+ exploded: false,
130
+ update() {
131
+ this.trail.push({ x: this.x, y: this.y });
132
+ if (this.trail.length > this.trailLength) {
133
+ this.trail.shift();
134
+ }
135
+ this.x += this.vx;
136
+ this.y += this.vy;
137
+ this.vy += 0.02;
138
+ if (this.vy >= 0 || this.y <= this.targetY) {
139
+ this.explode();
140
+ return false;
141
+ }
142
+ return true;
143
+ },
144
+ explode() {
145
+ const numParticles = randInt(50, 150);
146
+ const particles: ParticleType[] = [];
147
+ for (let i = 0; i < numParticles; i++) {
148
+ const particleAngle = rand(0, Math.PI * 2);
149
+ const localParticleSpeed = getValueByRange(particleSpeed);
150
+ const localParticleSize = getValueByRange(particleSize);
151
+ particles.push(
152
+ createParticle(
153
+ this.x,
154
+ this.y,
155
+ this.color,
156
+ localParticleSpeed,
157
+ particleAngle,
158
+ 0.05,
159
+ 0.98,
160
+ localParticleSize
161
+ )
162
+ );
163
+ }
164
+ onExplode(particles);
165
+ },
166
+ draw(ctx: CanvasRenderingContext2D) {
167
+ ctx.save();
168
+ ctx.beginPath();
169
+ if (this.trail.length > 1) {
170
+ ctx.moveTo(this.trail[0].x, this.trail[0].y);
171
+ for (const point of this.trail) {
172
+ ctx.lineTo(point.x, point.y);
173
+ }
174
+ } else {
175
+ ctx.moveTo(this.x, this.y);
176
+ ctx.lineTo(this.x, this.y);
177
+ }
178
+ ctx.strokeStyle = this.color;
179
+ ctx.lineWidth = this.size;
180
+ ctx.lineCap = "round";
181
+ ctx.stroke();
182
+ ctx.restore();
183
+ },
184
+ };
185
+ };
186
+
187
+ const getValueByRange = (
188
+ range: { min: number; max: number } | number
189
+ ): number => {
190
+ if (typeof range === "number") {
191
+ return range;
192
+ }
193
+ return rand(range.min, range.max);
194
+ };
195
+
196
+ const getColor = (color: string | string[] | undefined): string => {
197
+ if (Array.isArray(color)) {
198
+ return color[randInt(0, color.length)];
199
+ }
200
+ return color ?? randColor();
201
+ };
202
+
203
+ interface FireworksBackgroundProps
204
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "color"> {
205
+ canvasProps?: React.HTMLAttributes<HTMLCanvasElement>;
206
+ population?: number;
207
+ color?: string | string[];
208
+ fireworkSpeed?: { min: number; max: number } | number;
209
+ fireworkSize?: { min: number; max: number } | number;
210
+ particleSpeed?: { min: number; max: number } | number;
211
+ particleSize?: { min: number; max: number } | number;
212
+ }
213
+
214
+ const FireworksBackground = React.forwardRef<
215
+ HTMLDivElement,
216
+ FireworksBackgroundProps
217
+ >(
218
+ (
219
+ {
220
+ className,
221
+ canvasProps,
222
+ population = 1,
223
+ color,
224
+ fireworkSpeed = { min: 4, max: 8 },
225
+ fireworkSize = { min: 2, max: 5 },
226
+ particleSpeed = { min: 2, max: 7 },
227
+ particleSize = { min: 1, max: 5 },
228
+ ...props
229
+ },
230
+ ref
231
+ ) => {
232
+ const canvasRef = React.useRef<HTMLCanvasElement>(null);
233
+ const containerRef = React.useRef<HTMLDivElement>(null);
234
+ React.useImperativeHandle(
235
+ ref,
236
+ () => containerRef.current as HTMLDivElement
237
+ );
238
+
239
+ React.useEffect(() => {
240
+ const canvas = canvasRef.current;
241
+ const container = containerRef.current;
242
+ if (!canvas || !container) return;
243
+ const ctx = canvas.getContext("2d");
244
+ if (!ctx) return;
245
+
246
+ let maxX = window.innerWidth;
247
+ let ratio = container.offsetHeight / container.offsetWidth;
248
+ let maxY = maxX * ratio;
249
+ canvas.width = maxX;
250
+ canvas.height = maxY;
251
+
252
+ const setCanvasSize = () => {
253
+ maxX = window.innerWidth;
254
+ ratio = container.offsetHeight / container.offsetWidth;
255
+ maxY = maxX * ratio;
256
+ canvas.width = maxX;
257
+ canvas.height = maxY;
258
+ };
259
+ window.addEventListener("resize", setCanvasSize);
260
+
261
+ const explosions: ParticleType[] = [];
262
+ const fireworks: FireworkType[] = [];
263
+
264
+ const handleExplosion = (particles: ParticleType[]) => {
265
+ explosions.push(...particles);
266
+ };
267
+
268
+ const launchFirework = () => {
269
+ const x = rand(maxX * 0.1, maxX * 0.9);
270
+ const y = maxY;
271
+ const targetY = rand(maxY * 0.1, maxY * 0.4);
272
+ const fireworkColor = getColor(color);
273
+ const speed = getValueByRange(fireworkSpeed);
274
+ const size = getValueByRange(fireworkSize);
275
+ fireworks.push(
276
+ createFirework(
277
+ x,
278
+ y,
279
+ targetY,
280
+ fireworkColor,
281
+ speed,
282
+ size,
283
+ particleSpeed,
284
+ particleSize,
285
+ handleExplosion
286
+ )
287
+ );
288
+ const timeout = rand(300, 800) / population;
289
+ setTimeout(launchFirework, timeout);
290
+ };
291
+
292
+ launchFirework();
293
+
294
+ let animationFrameId: number;
295
+ const animate = () => {
296
+ ctx.clearRect(0, 0, maxX, maxY);
297
+
298
+ for (let i = fireworks.length - 1; i >= 0; i--) {
299
+ const firework = fireworks[i];
300
+ if (!firework.update()) {
301
+ fireworks.splice(i, 1);
302
+ } else {
303
+ firework.draw(ctx);
304
+ }
305
+ }
306
+
307
+ for (let i = explosions.length - 1; i >= 0; i--) {
308
+ const particle = explosions[i];
309
+ particle.update();
310
+ if (particle.isAlive()) {
311
+ particle.draw(ctx);
312
+ } else {
313
+ explosions.splice(i, 1);
314
+ }
315
+ }
316
+
317
+ animationFrameId = requestAnimationFrame(animate);
318
+ };
319
+
320
+ animate();
321
+
322
+ const handleClick = (event: MouseEvent) => {
323
+ const x = event.clientX;
324
+ const y = maxY;
325
+ const targetY = event.clientY;
326
+ const fireworkColor = getColor(color);
327
+ const speed = getValueByRange(fireworkSpeed);
328
+ const size = getValueByRange(fireworkSize);
329
+ fireworks.push(
330
+ createFirework(
331
+ x,
332
+ y,
333
+ targetY,
334
+ fireworkColor,
335
+ speed,
336
+ size,
337
+ particleSpeed,
338
+ particleSize,
339
+ handleExplosion
340
+ )
341
+ );
342
+ };
343
+
344
+ container.addEventListener("click", handleClick);
345
+
346
+ return () => {
347
+ window.removeEventListener("resize", setCanvasSize);
348
+ container.removeEventListener("click", handleClick);
349
+ cancelAnimationFrame(animationFrameId);
350
+ };
351
+ }, [
352
+ population,
353
+ color,
354
+ fireworkSpeed,
355
+ fireworkSize,
356
+ particleSpeed,
357
+ particleSize,
358
+ ]);
359
+
360
+ return (
361
+ <div
362
+ ref={containerRef}
363
+ className={cn("relative size-full overflow-hidden", className)}
364
+ {...props}
365
+ >
366
+ <canvas
367
+ {...canvasProps}
368
+ ref={canvasRef}
369
+ className={cn("absolute inset-0 size-full", canvasProps?.className)}
370
+ />
371
+ </div>
372
+ );
373
+ }
374
+ );
375
+
376
+ FireworksBackground.displayName = "FireworksBackground";
377
+
378
+ export { FireworksBackground, type FireworksBackgroundProps };
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ type HTMLMotionProps,
6
+ type Transition,
7
+ type Variant,
8
+ motion,
9
+ } from "motion/react";
10
+
11
+ import { cn } from "@/lib/utils";
12
+
13
+ type FlipDirection = "top" | "bottom" | "left" | "righ";
14
+
15
+ interface FlipButtonProps extends HTMLMotionProps<"button"> {
16
+ frontText: string;
17
+ backText: string;
18
+ transition?: Transition;
19
+ frontClassName?: string;
20
+ backClassName?: string;
21
+ from?: FlipDirection;
22
+ }
23
+
24
+ const defaultSpanClassName =
25
+ "absolute inset-0 flex items-center justify-center rounded-lg";
26
+
27
+ const FlipButton = React.forwardRef<HTMLButtonElement, FlipButtonProps>(
28
+ (
29
+ {
30
+ frontText,
31
+ backText,
32
+ transition = { type: "spring", stiffness: 280, damping: 20 },
33
+ className,
34
+ frontClassName,
35
+ backClassName,
36
+ from = "top",
37
+ ...props
38
+ },
39
+ ref
40
+ ) => {
41
+ const isVertical = from === "top" || from === "bottom";
42
+ const rotateAxis = isVertical ? "rotateX" : "rotateY";
43
+
44
+ const frontOffset = from === "top" || from === "left" ? "50%" : "-50%";
45
+ const backOffset = from === "top" || from === "left" ? "-50%" : "50%";
46
+
47
+ const buildVariant = (
48
+ opacity: number,
49
+ rotation: number,
50
+ offset: string | null = null
51
+ ): Variant => ({
52
+ opacity,
53
+ [rotateAxis]: rotation,
54
+ ...(isVertical && offset !== null ? { y: offset } : {}),
55
+ ...(!isVertical && offset !== null ? { x: offset } : {}),
56
+ });
57
+
58
+ const frontVariants = {
59
+ initial: buildVariant(1, 0, "0%"),
60
+ hover: buildVariant(0, 90, frontOffset),
61
+ };
62
+
63
+ const backVariants = {
64
+ initial: buildVariant(0, 90, backOffset),
65
+ hover: buildVariant(1, 0, "0%"),
66
+ };
67
+
68
+ return (
69
+ <motion.button
70
+ ref={ref}
71
+ initial="initial"
72
+ whileHover="hover"
73
+ whileTap={{ scale: 0.95 }}
74
+ className={cn(
75
+ "relative inline-block h-10 px-4 py-2 text-sm font-medium cursor-pointer perspective-[1000px] focus:outline-none",
76
+ className
77
+ )}
78
+ {...props}
79
+ >
80
+ <motion.span
81
+ variants={frontVariants}
82
+ transition={transition}
83
+ className={cn(
84
+ defaultSpanClassName,
85
+ "bg-muted text-black dark:text-white",
86
+ frontClassName
87
+ )}
88
+ >
89
+ {frontText}
90
+ </motion.span>
91
+ <motion.span
92
+ variants={backVariants}
93
+ transition={transition}
94
+ className={cn(
95
+ defaultSpanClassName,
96
+ "bg-primary text-primary-foreground",
97
+ backClassName
98
+ )}
99
+ >
100
+ {backText}
101
+ </motion.span>
102
+ <span className="invisible">{frontText}</span>
103
+ </motion.button>
104
+ );
105
+ }
106
+ );
107
+
108
+ FlipButton.displayName = "FlipButton";
109
+
110
+ export { FlipButton, type FlipButtonProps, type FlipDirection };
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { HTMLMotionProps, motion, type Transition } from "motion/react";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ interface GradientBackgroundProps extends HTMLMotionProps<"div"> {
9
+ transition?: Transition;
10
+ }
11
+
12
+ const GradientBackground = React.forwardRef<
13
+ HTMLDivElement,
14
+ GradientBackgroundProps
15
+ >(
16
+ (
17
+ {
18
+ className,
19
+ transition = { duration: 15, ease: "easeInOut", repeat: Infinity },
20
+ ...props
21
+ },
22
+ ref
23
+ ) => {
24
+ return (
25
+ <motion.div
26
+ ref={ref}
27
+ className={cn(
28
+ "size-full bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 bg-[length:400%_400%]",
29
+ className
30
+ )}
31
+ animate={{
32
+ backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
33
+ }}
34
+ transition={transition}
35
+ {...props}
36
+ />
37
+ );
38
+ }
39
+ );
40
+
41
+ GradientBackground.displayName = "GradientBackground";
42
+
43
+ export { GradientBackground, type GradientBackgroundProps };
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { motion, type Transition } from "motion/react";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ interface GradientTextProps extends React.HTMLAttributes<HTMLSpanElement> {
9
+ text: string;
10
+ gradient?: string;
11
+ neon?: boolean;
12
+ transition?: Transition;
13
+ }
14
+
15
+ const GradientText = React.forwardRef<HTMLSpanElement, GradientTextProps>(
16
+ (
17
+ {
18
+ text,
19
+ className,
20
+ gradient = "linear-gradient(90deg, #3b82f6 0%, #a855f7 20%, #ec4899 50%, #a855f7 80%, #3b82f6 100%)",
21
+ neon = false,
22
+ transition = { duration: 50, repeat: Infinity, ease: "linear" },
23
+ ...props
24
+ },
25
+ ref
26
+ ) => {
27
+ const baseStyle: React.CSSProperties = {
28
+ backgroundImage: gradient,
29
+ };
30
+
31
+ return (
32
+ <span
33
+ ref={ref}
34
+ className={cn("relative inline-block", className)}
35
+ {...props}
36
+ >
37
+ <motion.span
38
+ className="m-0 text-transparent bg-clip-text bg-[length:700%_100%] bg-[position:0%_0%]"
39
+ style={baseStyle}
40
+ initial={{ backgroundPosition: "0% 0%" }}
41
+ animate={{ backgroundPosition: "500% 100%" }}
42
+ transition={transition}
43
+ >
44
+ {text}
45
+ </motion.span>
46
+
47
+ {neon && (
48
+ <motion.span
49
+ className="m-0 absolute top-0 left-0 text-transparent bg-clip-text blur-[8px] mix-blend-plus-lighter bg-[length:700%_100%] bg-[position:0%_0%]"
50
+ style={baseStyle}
51
+ initial={{ backgroundPosition: "0% 0%" }}
52
+ animate={{ backgroundPosition: "500% 100%" }}
53
+ transition={transition}
54
+ >
55
+ {text}
56
+ </motion.span>
57
+ )}
58
+ </span>
59
+ );
60
+ }
61
+ );
62
+
63
+ GradientText.displayName = "GradientText";
64
+
65
+ export { GradientText, type GradientTextProps };
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ motion,
6
+ useInView,
7
+ type HTMLMotionProps,
8
+ type Transition,
9
+ type UseInViewOptions,
10
+ } from "motion/react";
11
+
12
+ import { cn } from "@/lib/utils";
13
+
14
+ interface HighlightTextProps extends HTMLMotionProps<"span"> {
15
+ text: string;
16
+ inView?: boolean;
17
+ inViewMargin?: UseInViewOptions["margin"];
18
+ inViewOnce?: boolean;
19
+ transition?: Transition;
20
+ }
21
+
22
+ const animation = { backgroundSize: "100% 100%" };
23
+
24
+ const HighlightText = React.forwardRef<HTMLSpanElement, HighlightTextProps>(
25
+ (
26
+ {
27
+ text,
28
+ className,
29
+ inView = false,
30
+ inViewMargin = "0px",
31
+ transition = { duration: 2, ease: "easeInOut" },
32
+ ...props
33
+ },
34
+ ref
35
+ ) => {
36
+ const localRef = React.useRef<HTMLSpanElement>(null);
37
+ React.useImperativeHandle(ref, () => localRef.current as HTMLSpanElement);
38
+
39
+ const inViewResult = useInView(localRef, {
40
+ once: true,
41
+ margin: inViewMargin,
42
+ });
43
+ const isInView = !inView || inViewResult;
44
+
45
+ return (
46
+ <motion.span
47
+ ref={localRef}
48
+ initial={{
49
+ backgroundSize: "0% 100%",
50
+ }}
51
+ animate={isInView ? animation : undefined}
52
+ transition={transition}
53
+ style={{
54
+ backgroundRepeat: "no-repeat",
55
+ backgroundPosition: "left center",
56
+ display: "inline",
57
+ }}
58
+ className={cn(
59
+ `relative inline-block px-2 py-1 rounded-lg bg-gradient-to-r from-blue-100 to-purple-100 dark:from-blue-500 dark:to-purple-500`,
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ {text}
65
+ </motion.span>
66
+ );
67
+ }
68
+ );
69
+ HighlightText.displayName = "HighlightText";
70
+
71
+ export { HighlightText, type HighlightTextProps };