abhishek-portfolio-template 1.0.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 (101) hide show
  1. package/README.md +59 -0
  2. package/bin/cli.js +54 -0
  3. package/package.json +27 -0
  4. package/template/components.json +22 -0
  5. package/template/next.config.ts +79 -0
  6. package/template/package.json +43 -0
  7. package/template/postcss.config.js +6 -0
  8. package/template/public/BoliviaSignature-ZpWnz.ttf +0 -0
  9. package/template/public/Gemini_Generated_Image_xc97toxc97toxc97.png +0 -0
  10. package/template/public/Hendrigo.otf +0 -0
  11. package/template/public/audiomass-output.mp3 +0 -0
  12. package/template/public/file.svg +1 -0
  13. package/template/public/globe.svg +1 -0
  14. package/template/public/googlec77e59474f5a09cb.html +1 -0
  15. package/template/public/icon-192x192.png +0 -0
  16. package/template/public/icon-512x512.png +0 -0
  17. package/template/public/next.svg +1 -0
  18. package/template/public/paper sound .mpeg +0 -0
  19. package/template/public/removebg.png +0 -0
  20. package/template/public/resume.pdf +0 -0
  21. package/template/public/sw.js +1 -0
  22. package/template/public/swe-worker-5c72df51bb1f6ee0.js +1 -0
  23. package/template/public/vercel.svg +1 -0
  24. package/template/public/window.svg +1 -0
  25. package/template/public/workbox-f1770938.js +1 -0
  26. package/template/src/app/about/page.tsx +91 -0
  27. package/template/src/app/actions/optimize-text.ts +54 -0
  28. package/template/src/app/gaming/page.tsx +308 -0
  29. package/template/src/app/github/[username]/page.tsx +97 -0
  30. package/template/src/app/globals.css +321 -0
  31. package/template/src/app/layout.tsx +39 -0
  32. package/template/src/app/manifest.ts +25 -0
  33. package/template/src/app/not-found.tsx +16 -0
  34. package/template/src/app/page.tsx +28 -0
  35. package/template/src/app/robots.ts +12 -0
  36. package/template/src/app/sitemap.ts +38 -0
  37. package/template/src/app/template.tsx +5 -0
  38. package/template/src/app/works/[slug]/page.tsx +50 -0
  39. package/template/src/app/works/client.tsx +44 -0
  40. package/template/src/app/works/page.tsx +24 -0
  41. package/template/src/components/about/about-client.tsx +259 -0
  42. package/template/src/components/home/bento-gallery.tsx +52 -0
  43. package/template/src/components/home/contact-section.tsx +34 -0
  44. package/template/src/components/home/craft-card.tsx +18 -0
  45. package/template/src/components/home/featured-projects.tsx +186 -0
  46. package/template/src/components/home/focus-card.tsx +171 -0
  47. package/template/src/components/home/identity-card.tsx +45 -0
  48. package/template/src/components/home/philosophy-card.tsx +104 -0
  49. package/template/src/components/home/skills-in-motion.tsx +109 -0
  50. package/template/src/components/home/tech-stack-marquee.tsx +56 -0
  51. package/template/src/components/ui/3d-folder.tsx +569 -0
  52. package/template/src/components/ui/avatar.tsx +50 -0
  53. package/template/src/components/ui/badge.tsx +36 -0
  54. package/template/src/components/ui/basic-avatar.tsx +12 -0
  55. package/template/src/components/ui/button.tsx +117 -0
  56. package/template/src/components/ui/clipboard-secret.tsx +39 -0
  57. package/template/src/components/ui/command-menu.tsx +519 -0
  58. package/template/src/components/ui/command-palette.tsx +152 -0
  59. package/template/src/components/ui/consciousness-mode.tsx +200 -0
  60. package/template/src/components/ui/copy-code-button.tsx +135 -0
  61. package/template/src/components/ui/display-cards.tsx +70 -0
  62. package/template/src/components/ui/dotted-map.tsx +128 -0
  63. package/template/src/components/ui/dropdown-menu.tsx +200 -0
  64. package/template/src/components/ui/emoji-rating.tsx +123 -0
  65. package/template/src/components/ui/exit-message.tsx +50 -0
  66. package/template/src/components/ui/image-zoom-overlay.tsx +178 -0
  67. package/template/src/components/ui/input-otp.tsx +71 -0
  68. package/template/src/components/ui/kbd.tsx +87 -0
  69. package/template/src/components/ui/location-tag.tsx +232 -0
  70. package/template/src/components/ui/minimal-testimonial.tsx +97 -0
  71. package/template/src/components/ui/mobile-menu.tsx +191 -0
  72. package/template/src/components/ui/navbar.tsx +148 -0
  73. package/template/src/components/ui/page-transition.tsx +24 -0
  74. package/template/src/components/ui/pixeleted-404-not-found.tsx +110 -0
  75. package/template/src/components/ui/preloader-wrapper.tsx +102 -0
  76. package/template/src/components/ui/preloader.tsx +104 -0
  77. package/template/src/components/ui/project-contributors.tsx +57 -0
  78. package/template/src/components/ui/scroll-area.tsx +117 -0
  79. package/template/src/components/ui/signature.tsx +173 -0
  80. package/template/src/components/ui/smooth-scroll.tsx +31 -0
  81. package/template/src/components/ui/social-icons.tsx +103 -0
  82. package/template/src/components/ui/social-stories.tsx +394 -0
  83. package/template/src/components/ui/sound-constants.ts +1 -0
  84. package/template/src/components/ui/text-explode.tsx +188 -0
  85. package/template/src/components/ui/toast.tsx +80 -0
  86. package/template/src/components/ui/tooltip.tsx +30 -0
  87. package/template/src/components/ui/user-location.tsx +151 -0
  88. package/template/src/components/ui/vertical-image-stack.tsx +345 -0
  89. package/template/src/components/works/changelog-overlay.tsx +212 -0
  90. package/template/src/components/works/currently-working-card.tsx +130 -0
  91. package/template/src/components/works/project-details-view.tsx +464 -0
  92. package/template/src/components/works/project-grid.tsx +81 -0
  93. package/template/src/fonts/BoliviaSignature-ZpWnz.ttf +0 -0
  94. package/template/src/fonts/Hendrigo.otf +0 -0
  95. package/template/src/lib/data.ts +61 -0
  96. package/template/src/lib/fonts.ts +14 -0
  97. package/template/src/lib/github.ts +15 -0
  98. package/template/src/lib/supabase.ts +11 -0
  99. package/template/src/lib/utils.ts +6 -0
  100. package/template/tailwind.config.ts +31 -0
  101. package/template/tsconfig.json +34 -0
@@ -0,0 +1,394 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect, useCallback, useRef } from "react"
4
+ import { createPortal } from "react-dom"
5
+ import { motion, AnimatePresence } from "framer-motion"
6
+ import { ArrowUpRight, X, Loader2 } from "lucide-react"
7
+ import Image from "next/image"
8
+ import { supabase } from "@/lib/supabase"
9
+ import { cn } from "@/lib/utils"
10
+
11
+ export type SocialPlatform = "linkedin" | "instagram"
12
+
13
+ export interface Story {
14
+ id: string
15
+ platform: SocialPlatform
16
+ mediaUrl: string
17
+ linkUrl: string
18
+ duration?: number // duration in seconds
19
+ date?: string
20
+ caption?: string
21
+ }
22
+
23
+ const PROFILE = {
24
+ name: "Abhishek Singh",
25
+ avatarUrl: "https://res.cloudinary.com/dap0u41dz/image/upload/v1766771167/file_00000000d51472078b7e2f9d883a6674_majhmb.jpg",
26
+ }
27
+
28
+ export function SocialStories({ id = "default" }: { id?: string }) {
29
+ const [stories, setStories] = useState<Story[]>([])
30
+ const [isOpen, setIsOpen] = useState(false)
31
+ const [currentIndex, setCurrentIndex] = useState(0)
32
+ const [isPaused, setIsPaused] = useState(false)
33
+ const [isMediaLoaded, setIsMediaLoaded] = useState(false)
34
+ const [mounted, setMounted] = useState(false)
35
+ const [dynamicDuration, setDynamicDuration] = useState<number | null>(null)
36
+ const [isFetchLoading, setIsFetchLoading] = useState(true)
37
+
38
+ // Timing refs for high-performance animation
39
+ const startTimeRef = useRef<number | null>(null)
40
+ const pausedAtRef = useRef<number | null>(null)
41
+ const rafRef = useRef<number | null>(null)
42
+ const lastTimeRef = useRef<number>(Date.now())
43
+ const progressRef = useRef(0)
44
+
45
+ // Direct DOM manipulation ref for performance
46
+ const activeProgressBarRef = useRef<HTMLDivElement>(null)
47
+
48
+ useEffect(() => {
49
+ setMounted(true)
50
+ fetchStories()
51
+ }, [])
52
+
53
+ const fetchStories = async () => {
54
+ setIsFetchLoading(true)
55
+ try {
56
+ const { data } = await supabase
57
+ .from("social_stories")
58
+ .select("*")
59
+ .order("display_order", { ascending: true })
60
+
61
+ if (data) {
62
+ setStories(data.map(s => ({
63
+ id: s.id,
64
+ platform: s.platform as SocialPlatform,
65
+ mediaUrl: s.media_url,
66
+ linkUrl: s.link_url,
67
+ caption: s.caption,
68
+ duration: 5
69
+ })))
70
+ }
71
+ } finally {
72
+ setIsFetchLoading(false)
73
+ }
74
+ }
75
+
76
+ const currentStory = stories[currentIndex]
77
+
78
+ // Priority: Dynamic (video detected) > DB provided > Default (5s)
79
+ const defaultDuration = 5
80
+ const durationMs = dynamicDuration ?? (currentStory?.duration ?? defaultDuration) * 1000
81
+
82
+ const stopAnimation = () => {
83
+ if (rafRef.current) {
84
+ cancelAnimationFrame(rafRef.current)
85
+ rafRef.current = null
86
+ }
87
+ }
88
+
89
+ const resetTiming = () => {
90
+ startTimeRef.current = null
91
+ // pausedAtRef.current = null // Not used in current draft but good to reset if added back
92
+ setIsMediaLoaded(false)
93
+ if (activeProgressBarRef.current) {
94
+ activeProgressBarRef.current.style.width = "0%"
95
+ }
96
+ progressRef.current = 0
97
+ }
98
+
99
+ const isVideoUrl = (url: string) => {
100
+ return url.match(/\.(mp4|webm|ogg|mov|m4v)$|^https?:\/\/res\.cloudinary\.com\/.*\/video\/upload\//i);
101
+ };
102
+
103
+ const goToNext = useCallback(() => {
104
+ stopAnimation()
105
+ resetTiming()
106
+ setDynamicDuration(null)
107
+
108
+ if (currentIndex < stories.length - 1) {
109
+ setCurrentIndex(i => i + 1)
110
+ } else {
111
+ setIsOpen(false)
112
+ setCurrentIndex(0)
113
+ }
114
+ }, [currentIndex, stories.length])
115
+
116
+ const goToPrev = useCallback(() => {
117
+ if (currentIndex === 0) return
118
+ stopAnimation()
119
+ resetTiming()
120
+ setDynamicDuration(null)
121
+ setCurrentIndex(i => i - 1)
122
+ }, [currentIndex])
123
+
124
+ useEffect(() => {
125
+ // Reset timing when index changes
126
+ lastTimeRef.current = Date.now()
127
+ progressRef.current = 0
128
+ }, [currentIndex])
129
+
130
+ useEffect(() => {
131
+ if (!isOpen || !currentStory) return
132
+
133
+ let animationFrameId: number
134
+
135
+ const animate = () => {
136
+ if (!isPaused && isMediaLoaded) {
137
+ const now = Date.now()
138
+ // Limit delta to prevent huge jumps if tab was inactive
139
+ const delta = Math.min(now - lastTimeRef.current, 100)
140
+ lastTimeRef.current = now
141
+
142
+ progressRef.current += delta
143
+ const progressPercent = Math.min((progressRef.current / durationMs) * 100, 100)
144
+
145
+ if (activeProgressBarRef.current) {
146
+ activeProgressBarRef.current.style.width = `${progressPercent}%`
147
+ }
148
+
149
+ if (progressRef.current >= durationMs) {
150
+ goToNext()
151
+ }
152
+ } else {
153
+ lastTimeRef.current = Date.now()
154
+ }
155
+
156
+ animationFrameId = requestAnimationFrame(animate)
157
+ }
158
+
159
+ animationFrameId = requestAnimationFrame(animate)
160
+
161
+ return () => {
162
+ cancelAnimationFrame(animationFrameId)
163
+ }
164
+ }, [isPaused, durationMs, goToNext, isOpen, currentIndex, isMediaLoaded, currentStory])
165
+
166
+ const handleTap = (e: React.MouseEvent<HTMLDivElement>) => {
167
+ if ((e.target as HTMLElement).closest('button')) {
168
+ return
169
+ }
170
+
171
+ const width = e.currentTarget.offsetWidth
172
+ const x = e.nativeEvent.offsetX
173
+
174
+ if (x < width / 3) {
175
+ goToPrev()
176
+ } else {
177
+ goToNext()
178
+ }
179
+ }
180
+
181
+ const toggleOpen = () => {
182
+ if (stories.length > 0) {
183
+ setIsOpen(!isOpen)
184
+ }
185
+ }
186
+
187
+ if (!mounted) return null
188
+ // removed early return to show trigger immediately
189
+
190
+ const isCurrentVideo = currentStory ? isVideoUrl(currentStory.mediaUrl) : false;
191
+
192
+ return (
193
+ <>
194
+ {/* Trigger in Navbar - Gold Border Ring */}
195
+ <div className="w-full h-full relative flex items-center justify-center z-50 group">
196
+ <AnimatePresence>
197
+ {!isOpen && (
198
+ <motion.div
199
+ // Removed layoutId to prevent visibility conflics
200
+ key="trigger"
201
+ className={cn(
202
+ "absolute inset-0 cursor-pointer rounded-full overflow-hidden transition-all duration-300",
203
+ // Gold border integration
204
+ "border-[1.5px]",
205
+ isFetchLoading
206
+ ? "border-white/10 opacity-50 grayscale"
207
+ : "border-[#007AFF] hover:border-[#007AFF] opacity-100 grayscale-0 shadow-[0_0_10px_rgba(0,122,255,0.3)]"
208
+ )}
209
+ onClick={() => !isFetchLoading && stories.length > 0 && setIsOpen(true)}
210
+ initial={{ opacity: 0, scale: 0.8 }}
211
+ animate={{ opacity: 1, scale: 1 }}
212
+ exit={{ opacity: 0, scale: 0.8 }}
213
+ transition={{ duration: 0.2 }}
214
+ >
215
+ <Image
216
+ src={PROFILE.avatarUrl}
217
+ alt={PROFILE.name}
218
+ fill
219
+ className="object-cover p-[2px] rounded-full" // Slight padding inside border
220
+ priority
221
+ />
222
+
223
+ {/* Loading / Active Pulse Overlay */}
224
+ {isFetchLoading && (
225
+ <motion.div
226
+ className="absolute inset-0 bg-white/10"
227
+ animate={{ opacity: [0, 0.2, 0] }}
228
+ transition={{ duration: 1.5, repeat: Infinity }}
229
+ />
230
+ )}
231
+ </motion.div>
232
+ )}
233
+ </AnimatePresence>
234
+ </div>
235
+
236
+ {/* Portal for Expanded View */}
237
+ {createPortal(
238
+ <AnimatePresence>
239
+ {isOpen && (
240
+ <div className="fixed inset-0 z-[100] flex items-center justify-center">
241
+ {/* Backdrop */}
242
+ <motion.div
243
+ initial={{ opacity: 0 }}
244
+ animate={{ opacity: 1 }}
245
+ exit={{ opacity: 0 }}
246
+ className="absolute inset-0 bg-black/80 backdrop-blur-md"
247
+ onClick={() => setIsOpen(false)}
248
+ />
249
+
250
+ {/* Card - Responsive Size */}
251
+ <motion.div
252
+ key="card"
253
+ initial={{ opacity: 0, scale: 0.9, y: 20 }}
254
+ animate={{ opacity: 1, scale: 1, y: 0 }}
255
+ exit={{ opacity: 0, scale: 0.9, y: 20 }}
256
+ transition={{ type: "spring", damping: 25, stiffness: 300 }}
257
+ className="relative w-[90vw] h-[65vh] md:w-[380px] md:h-[650px] bg-black rounded-[32px] overflow-hidden shadow-2xl z-10"
258
+ drag="y"
259
+ dragConstraints={{ top: 0, bottom: 0 }}
260
+ onDragEnd={(e, { offset, velocity }) => {
261
+ if (offset.y > 100 || velocity.y > 500) {
262
+ setIsOpen(false)
263
+ }
264
+ }}
265
+ >
266
+ <div className="absolute inset-0 w-full h-full"
267
+ onMouseEnter={() => setIsPaused(true)}
268
+ onMouseLeave={() => setIsPaused(false)}
269
+ onMouseDown={() => setIsPaused(true)}
270
+ onMouseUp={(e) => {
271
+ setIsPaused(false)
272
+ handleTap(e)
273
+ }}
274
+ onTouchStart={() => setIsPaused(true)}
275
+ onTouchEnd={() => setIsPaused(false)}
276
+ >
277
+ {/* Loading State Spinner */}
278
+ {!isMediaLoaded && (
279
+ <div className="absolute inset-0 z-20 flex items-center justify-center bg-black/20">
280
+ <Loader2 className="w-8 h-8 text-white/50 animate-spin" />
281
+ </div>
282
+ )}
283
+
284
+ {/* Background Media */}
285
+ <AnimatePresence mode="wait">
286
+ <motion.div
287
+ key={currentStory.id}
288
+ initial={{ opacity: 0.6, scale: 1.05 }}
289
+ animate={{ opacity: 1, scale: 1 }}
290
+ exit={{ opacity: 0.6 }}
291
+ transition={{ duration: 0.3 }}
292
+ className="absolute inset-0 z-0"
293
+ >
294
+ {isCurrentVideo ? (
295
+ <video
296
+ src={currentStory.mediaUrl}
297
+ autoPlay
298
+ playsInline
299
+ loop
300
+ className="w-full h-full object-contain"
301
+ onLoadedData={(e) => {
302
+ const video = e.currentTarget;
303
+ setDynamicDuration(video.duration * 1000);
304
+ setIsMediaLoaded(true);
305
+ }}
306
+ />
307
+ ) : (
308
+ <Image
309
+ src={currentStory.mediaUrl}
310
+ alt={currentStory.caption || "Story"}
311
+ fill
312
+ className="object-cover"
313
+ priority
314
+ onLoad={() => setIsMediaLoaded(true)}
315
+ />
316
+ )}
317
+ <div className="absolute inset-0 bg-gradient-to-b from-black/60 via-transparent to-black/80 pointer-events-none" />
318
+ </motion.div>
319
+ </AnimatePresence>
320
+
321
+ {/* Content Overlay */}
322
+ <div className="absolute inset-0 z-10 flex flex-col p-5 pointer-events-none">
323
+ {/* Progress Bars */}
324
+ <div className="flex gap-1.5 mb-4">
325
+ {stories.map((story, idx) => (
326
+ <div key={story.id} className="h-0.5 flex-1 bg-white/30 rounded-full overflow-hidden backdrop-blur-sm">
327
+ <div
328
+ ref={idx === currentIndex ? activeProgressBarRef : null}
329
+ className="h-full bg-white shadow-[0_0_10px_rgba(255,255,255,0.5)] transition-all duration-100 ease-linear rounded-full"
330
+ style={{
331
+ width: idx < currentIndex ? "100%" : "0%"
332
+ }}
333
+ />
334
+ </div>
335
+ ))}
336
+ </div>
337
+
338
+ {/* Header */}
339
+ <div className="flex items-center justify-between pointer-events-auto">
340
+ <div className="flex items-center gap-2.5">
341
+ <div className="w-8 h-8 rounded-full border border-white/20 overflow-hidden relative">
342
+ <Image src={PROFILE.avatarUrl} alt={PROFILE.name} fill className="object-cover" />
343
+ </div>
344
+ <div className="flex flex-col">
345
+ <span className="text-white font-semibold text-xs leading-none">{PROFILE.name}</span>
346
+ <span className="text-white/60 text-[10px] leading-none mt-0.5">
347
+ {currentStory.platform === 'linkedin' ? 'via LinkedIn' : 'via Instagram'}
348
+ </span>
349
+ </div>
350
+ </div>
351
+
352
+ {/* Toggle Close Button */}
353
+ <button
354
+ onClick={(e) => {
355
+ e.stopPropagation()
356
+ setIsOpen(false)
357
+ }}
358
+ aria-label="Close Story"
359
+ className="w-8 h-8 flex items-center justify-center rounded-full bg-black/20 backdrop-blur-md text-white/80 hover:bg-black/40 hover:text-white transition-colors"
360
+ >
361
+ <X className="w-4 h-4" />
362
+ </button>
363
+ </div>
364
+
365
+ <div className="flex-1" />
366
+
367
+ {/* Footer */}
368
+ <div className="flex items-end justify-between gap-4 pb-1 pointer-events-auto">
369
+ <p className="text-white/90 text-sm font-medium line-clamp-3 drop-shadow-md flex-1">
370
+ {currentStory.caption}
371
+ </p>
372
+
373
+ <a
374
+ href={currentStory.linkUrl}
375
+ target="_blank"
376
+ rel="noopener noreferrer"
377
+ aria-label="Visit Story Link"
378
+ className="w-12 h-12 flex-shrink-0 flex items-center justify-center rounded-full bg-white text-black hover:scale-110 active:scale-95 transition-all shadow-lg group/btn"
379
+ onClick={(e) => e.stopPropagation()}
380
+ >
381
+ <ArrowUpRight className="w-5 h-5 group-hover/btn:rotate-45 transition-transform duration-300" strokeWidth={2.5} />
382
+ </a>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ </motion.div>
387
+ </div>
388
+ )}
389
+ </AnimatePresence>,
390
+ document.body
391
+ )}
392
+ </>
393
+ )
394
+ }
@@ -0,0 +1 @@
1
+ export const PAPER_SOUND_BASE64 = 'data:audio/mp3;base64,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';
@@ -0,0 +1,188 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef } from "react";
4
+ import { motion, useAnimationControls, Variants } from "framer-motion";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const containerVariants: Variants = {
9
+ initial: {
10
+ opacity: 1,
11
+ translateY: 0,
12
+ transition: {
13
+ duration: 0.5,
14
+ },
15
+ letterSpacing: "0px",
16
+ },
17
+ shrink: {
18
+ // Scale might need to be adjust according to font size for better effect
19
+ scale: 0.8,
20
+ letterSpacing: "-10%",
21
+ },
22
+ jitter: {
23
+ x: [0, -3, 3, -3, 3, 0],
24
+ y: [0, -2, 2, -2, 2, 0],
25
+ transition: {
26
+ duration: 0.5,
27
+ times: [0, 0.2, 0.4, 0.6, 0.8, 1],
28
+ ease: "easeInOut",
29
+ },
30
+ },
31
+ explode: {
32
+ scale: [0.7, 0.9, 1],
33
+ opacity: [1, 0.7, 0],
34
+ letterSpacing: "0px",
35
+ transition: {
36
+ times: [0, 0.9, 1],
37
+ },
38
+ },
39
+ end: {
40
+ scale: 1,
41
+ letterSpacing: "0px",
42
+ translateY: 50,
43
+ },
44
+ };
45
+
46
+ const createExplosion = ({ index, total }: { index: number; total: number }) => {
47
+ const direction = Math.random() > Math.random() ? -1 : 1;
48
+
49
+ // Increased horizontal spread
50
+ const x = Math.random() * 15 * total * direction;
51
+
52
+ // Increased vertical bias and radius for a more "earned" burst
53
+ const radius = total * 10;
54
+ const angleRange = Math.PI;
55
+ const angle = (index / (total - 1)) * angleRange;
56
+ // Bias significantly upward (-Math.sin) with more force
57
+ const y = radius * -Math.sin(angle) * (1 + Math.random() * 2);
58
+
59
+ const rotation = Math.random() * 720 * direction; // More rotation
60
+
61
+ return {
62
+ translateX: [0, x * 0.6, x * 0.9, x],
63
+ translateY: [0, y, y * 1.1, y * 0.8, y * 0.5], // Aggressive upward arc
64
+ rotate: [0, rotation * 0.5, rotation * 0.9, rotation],
65
+ scale: [1, 1.4, 2 + Math.random(), 2.5 + Math.random() * 2],
66
+ opacity: [1, 0.9, 0.4, 0],
67
+ };
68
+ };
69
+
70
+ const characterVariants: Variants = {
71
+ jitter: () => ({
72
+ x: [0, -3 + Math.random() * 6, 3 - Math.random() * 6, 0],
73
+ y: [0, -2 + Math.random() * 4, 2 - Math.random() * 4, 0],
74
+ transition: {
75
+ duration: 0.5,
76
+ times: [0, 0.33, 0.66, 1],
77
+ ease: "easeInOut",
78
+ },
79
+ }),
80
+ shrink: {
81
+ scale: 1.1,
82
+ },
83
+ explode: createExplosion,
84
+ end: {
85
+ translateY: 0,
86
+ translateX: 0,
87
+ rotate: 0,
88
+ scale: 1,
89
+ },
90
+ initial: {
91
+ opacity: 1,
92
+ },
93
+ };
94
+
95
+ const splitText = (text: string) => String(text).split(/(?:)/u);
96
+
97
+ export default function TextExplode({
98
+ text,
99
+ mode = "loop",
100
+ className,
101
+ trigger = false,
102
+ onComplete,
103
+ }: {
104
+ text: string;
105
+ className?: string;
106
+ mode?: "loop" | "hover" | "manual";
107
+ trigger?: boolean;
108
+ onComplete?: () => void;
109
+ }) {
110
+ const characters = splitText(text);
111
+ const controls = useAnimationControls();
112
+ const isPlaying = useRef(false);
113
+
114
+ const animateSequence = useCallback(async () => {
115
+ await controls.start("shrink", {
116
+ duration: 0.8,
117
+ ease: "easeOut",
118
+ });
119
+ // Removed jitter for a cleaner feel
120
+ await controls.start("explode", {
121
+ duration: 0.6,
122
+ ease: "easeOut",
123
+ });
124
+ await controls.start("end");
125
+ await controls.start("initial", {
126
+ delay: 0.2,
127
+ duration: 0.6,
128
+ ease: "easeOut",
129
+ });
130
+
131
+ if (mode === "loop") {
132
+ requestAnimationFrame(() => animateSequence());
133
+ } else {
134
+ isPlaying.current = false;
135
+ onComplete?.();
136
+ }
137
+ }, [mode, controls, onComplete]);
138
+
139
+ useEffect(() => {
140
+ if (!characters.length || mode === "hover" || mode === "manual") {
141
+ return;
142
+ }
143
+
144
+ animateSequence();
145
+ }, [characters.length, mode, animateSequence]);
146
+
147
+ useEffect(() => {
148
+ if (mode === "manual" && trigger && !isPlaying.current) {
149
+ isPlaying.current = true;
150
+ animateSequence();
151
+ }
152
+ }, [trigger, mode, animateSequence]);
153
+
154
+ return (
155
+ <motion.div
156
+ variants={containerVariants}
157
+ animate={controls}
158
+ onPointerDown={() => {
159
+ if (mode === "hover" && !isPlaying.current) {
160
+ isPlaying.current = true;
161
+ animateSequence();
162
+ }
163
+ }}
164
+ onMouseEnter={() => {
165
+ if (mode === "hover" && !isPlaying.current) {
166
+ isPlaying.current = true;
167
+ animateSequence();
168
+ }
169
+ }}
170
+ className={cn(
171
+ "flex items-center justify-center text-3xl tracking-normal text-foreground",
172
+ className,
173
+ )}
174
+ >
175
+ {characters.map((char, index) => (
176
+ <motion.span
177
+ key={index}
178
+ variants={characterVariants}
179
+ custom={{ index, total: characters.length }}
180
+ className="inline-block whitespace-pre"
181
+ >
182
+ {char === " " ? "\u00A0" : char}
183
+ </motion.span>
184
+ ))}
185
+ <span className="sr-only">{text}</span>
186
+ </motion.div>
187
+ );
188
+ }