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,200 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
+ import { Check, ChevronRight, Circle } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const DropdownMenu = DropdownMenuPrimitive.Root
10
+
11
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12
+
13
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group
14
+
15
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16
+
17
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub
18
+
19
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20
+
21
+ const DropdownMenuSubTrigger = React.forwardRef<
22
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
+ inset?: boolean
25
+ }
26
+ >(({ className, inset, children, ...props }, ref) => (
27
+ <DropdownMenuPrimitive.SubTrigger
28
+ ref={ref}
29
+ className={cn(
30
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
31
+ inset && "pl-8",
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronRight className="ml-auto h-4 w-4" />
38
+ </DropdownMenuPrimitive.SubTrigger>
39
+ ))
40
+ DropdownMenuSubTrigger.displayName =
41
+ DropdownMenuPrimitive.SubTrigger.displayName
42
+
43
+ const DropdownMenuSubContent = React.forwardRef<
44
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
45
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
46
+ >(({ className, ...props }, ref) => (
47
+ <DropdownMenuPrimitive.SubContent
48
+ ref={ref}
49
+ className={cn(
50
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
+ className,
52
+ )}
53
+ {...props}
54
+ />
55
+ ))
56
+ DropdownMenuSubContent.displayName =
57
+ DropdownMenuPrimitive.SubContent.displayName
58
+
59
+ const DropdownMenuContent = React.forwardRef<
60
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
61
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
62
+ >(({ className, sideOffset = 4, ...props }, ref) => (
63
+ <DropdownMenuPrimitive.Portal>
64
+ <DropdownMenuPrimitive.Content
65
+ ref={ref}
66
+ sideOffset={sideOffset}
67
+ className={cn(
68
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
69
+ className,
70
+ )}
71
+ {...props}
72
+ />
73
+ </DropdownMenuPrimitive.Portal>
74
+ ))
75
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76
+
77
+ const DropdownMenuItem = React.forwardRef<
78
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
79
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
80
+ inset?: boolean
81
+ }
82
+ >(({ className, inset, ...props }, ref) => (
83
+ <DropdownMenuPrimitive.Item
84
+ ref={ref}
85
+ className={cn(
86
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
87
+ inset && "pl-8",
88
+ className,
89
+ )}
90
+ {...props}
91
+ />
92
+ ))
93
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94
+
95
+ const DropdownMenuCheckboxItem = React.forwardRef<
96
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
97
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
98
+ >(({ className, children, checked, ...props }, ref) => (
99
+ <DropdownMenuPrimitive.CheckboxItem
100
+ ref={ref}
101
+ className={cn(
102
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
103
+ className,
104
+ )}
105
+ checked={checked}
106
+ {...props}
107
+ >
108
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
109
+ <DropdownMenuPrimitive.ItemIndicator>
110
+ <Check className="h-4 w-4" />
111
+ </DropdownMenuPrimitive.ItemIndicator>
112
+ </span>
113
+ {children}
114
+ </DropdownMenuPrimitive.CheckboxItem>
115
+ ))
116
+ DropdownMenuCheckboxItem.displayName =
117
+ DropdownMenuPrimitive.CheckboxItem.displayName
118
+
119
+ const DropdownMenuRadioItem = React.forwardRef<
120
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
121
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
122
+ >(({ className, children, ...props }, ref) => (
123
+ <DropdownMenuPrimitive.RadioItem
124
+ ref={ref}
125
+ className={cn(
126
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
127
+ className,
128
+ )}
129
+ {...props}
130
+ >
131
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
132
+ <DropdownMenuPrimitive.ItemIndicator>
133
+ <Circle className="h-2 w-2 fill-current" />
134
+ </DropdownMenuPrimitive.ItemIndicator>
135
+ </span>
136
+ {children}
137
+ </DropdownMenuPrimitive.RadioItem>
138
+ ))
139
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140
+
141
+ const DropdownMenuLabel = React.forwardRef<
142
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
143
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
144
+ inset?: boolean
145
+ }
146
+ >(({ className, inset, ...props }, ref) => (
147
+ <DropdownMenuPrimitive.Label
148
+ ref={ref}
149
+ className={cn(
150
+ "px-2 py-1.5 text-sm font-semibold",
151
+ inset && "pl-8",
152
+ className,
153
+ )}
154
+ {...props}
155
+ />
156
+ ))
157
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158
+
159
+ const DropdownMenuSeparator = React.forwardRef<
160
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
161
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
162
+ >(({ className, ...props }, ref) => (
163
+ <DropdownMenuPrimitive.Separator
164
+ ref={ref}
165
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
166
+ {...props}
167
+ />
168
+ ))
169
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170
+
171
+ const DropdownMenuShortcut = ({
172
+ className,
173
+ ...props
174
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
175
+ return (
176
+ <span
177
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
178
+ {...props}
179
+ />
180
+ )
181
+ }
182
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183
+
184
+ export {
185
+ DropdownMenu,
186
+ DropdownMenuTrigger,
187
+ DropdownMenuContent,
188
+ DropdownMenuItem,
189
+ DropdownMenuCheckboxItem,
190
+ DropdownMenuRadioItem,
191
+ DropdownMenuLabel,
192
+ DropdownMenuSeparator,
193
+ DropdownMenuShortcut,
194
+ DropdownMenuGroup,
195
+ DropdownMenuPortal,
196
+ DropdownMenuSub,
197
+ DropdownMenuSubContent,
198
+ DropdownMenuSubTrigger,
199
+ DropdownMenuRadioGroup,
200
+ }
@@ -0,0 +1,123 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ interface RatingInteractionProps {
7
+ className?: string
8
+ }
9
+
10
+ const ratingData = [
11
+ { emoji: "😔", label: "Terrible", color: "from-red-400 to-red-500", shadowColor: "shadow-red-500/30" },
12
+ { emoji: "😕", label: "Poor", color: "from-orange-400 to-orange-500", shadowColor: "shadow-orange-500/30" },
13
+ { emoji: "😐", label: "Okay", color: "from-yellow-400 to-yellow-500", shadowColor: "shadow-yellow-500/30" },
14
+ { emoji: "🙂", label: "Good", color: "from-lime-400 to-lime-500", shadowColor: "shadow-lime-500/30" },
15
+ { emoji: "😍", label: "Amazing", color: "from-emerald-400 to-emerald-500", shadowColor: "shadow-emerald-500/30" },
16
+ ]
17
+
18
+ export function RatingInteraction({ className }: RatingInteractionProps) {
19
+ const [rating, setRating] = useState(0)
20
+ const [hoverRating, setHoverRating] = useState(0)
21
+ const [submitted, setSubmitted] = useState(false)
22
+
23
+ const handleClick = async (value: number) => {
24
+ setRating(value)
25
+ setSubmitted(true)
26
+
27
+ try {
28
+ await fetch('/api/ratings', {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify({ rating: value })
32
+ })
33
+ } catch (e) {
34
+ console.error("Failed to submit rating", e)
35
+ }
36
+ }
37
+
38
+ const displayRating = hoverRating || rating
39
+
40
+ if (submitted) {
41
+ return (
42
+ <div className={cn("flex flex-col items-center gap-4 py-8 animate-in fade-in zoom-in duration-500", className)}>
43
+ <div className="w-16 h-16 mb-2"><img src="https://emojicdn.elk.sh/🎉?style=apple" className="w-full h-full object-contain" alt="Success" /></div>
44
+ <p className="text-white/60 font-medium">Thank you for your feedback!</p>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ return (
50
+ <div className={cn("flex flex-col items-center gap-6", className)}>
51
+ <div className="text-center space-y-1">
52
+ <p className="text-xs font-semibold uppercase tracking-[0.2em] text-white/40">How was your experience?</p>
53
+ </div>
54
+
55
+ {/* Emoji rating buttons */}
56
+ <div className="flex items-center gap-3">
57
+ {ratingData.map((item, i) => {
58
+ const value = i + 1
59
+ const isActive = value <= displayRating
60
+
61
+ return (
62
+ <button
63
+ key={value}
64
+ onClick={() => handleClick(value)}
65
+ onMouseEnter={() => setHoverRating(value)}
66
+ onMouseLeave={() => setHoverRating(0)}
67
+ className="group relative focus:outline-none"
68
+ aria-label={`Rate ${value}: ${item.label}`}
69
+ >
70
+ <div
71
+ className={cn(
72
+ "relative flex h-12 w-12 md:h-14 md:w-14 items-center justify-center rounded-2xl transition-all duration-300 ease-out bg-white/5 border border-white/5",
73
+ isActive ? "scale-110 bg-white/10" : "scale-100 group-hover:scale-105 group-hover:bg-white/10",
74
+ )}
75
+ >
76
+ {/* Emoji with smooth grayscale transition */}
77
+ <div
78
+ className={cn(
79
+ "w-8 h-8 md:w-10 md:h-10 transition-all duration-300 ease-out select-none flex items-center justify-center",
80
+ isActive
81
+ ? "grayscale-0 drop-shadow-lg scale-110"
82
+ : "grayscale opacity-40 group-hover:opacity-100 group-hover:grayscale-0",
83
+ )}
84
+ >
85
+ <img
86
+ src={`https://emojicdn.elk.sh/${item.emoji}?style=apple`}
87
+ alt={item.label}
88
+ className="w-full h-full object-contain pointer-events-none"
89
+ loading="lazy"
90
+ />
91
+ </div>
92
+ </div>
93
+ </button>
94
+ )
95
+ })}
96
+ </div>
97
+
98
+ <div className="relative h-6 w-32">
99
+ {/* Rating labels */}
100
+ {ratingData.map((item, i) => (
101
+ <div
102
+ key={i}
103
+ className={cn(
104
+ "absolute inset-0 flex items-center justify-center transition-all duration-300 ease-out",
105
+ displayRating === i + 1 ? "opacity-100 blur-0 scale-100" : "opacity-0 blur-md scale-105",
106
+ )}
107
+ >
108
+ <span className="text-xs font-medium tracking-wide text-white/60">{item.label}</span>
109
+ </div>
110
+ ))}
111
+ {/* Default Empty State Text (Optional, or just hide) */}
112
+ <div
113
+ className={cn(
114
+ "absolute inset-0 flex items-center justify-center transition-all duration-300 ease-out",
115
+ displayRating === 0 ? "opacity-100 blur-0 scale-100" : "opacity-0 blur-md scale-95",
116
+ )}
117
+ >
118
+ <span className="text-xs font-medium tracking-wide text-white/20">Select one</span>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+
6
+ export const ExitMessage = () => {
7
+ const [isVisible, setIsVisible] = useState(false);
8
+
9
+ useEffect(() => {
10
+ const handleMouseLeave = (e: MouseEvent) => {
11
+ // Check if cursor left from the top (Exit Intent)
12
+ if (e.clientY <= 0) {
13
+ setIsVisible(true);
14
+ }
15
+ };
16
+
17
+ const handleMouseEnter = () => {
18
+ // Hide if user comes back
19
+ setIsVisible(false);
20
+ };
21
+
22
+ document.documentElement.addEventListener("mouseleave", handleMouseLeave);
23
+ document.documentElement.addEventListener("mouseenter", handleMouseEnter);
24
+
25
+ return () => {
26
+ document.documentElement.removeEventListener("mouseleave", handleMouseLeave);
27
+ document.documentElement.removeEventListener("mouseenter", handleMouseEnter);
28
+ };
29
+ }, []);
30
+
31
+ return (
32
+ <AnimatePresence>
33
+ {isVisible && (
34
+ <motion.div
35
+ initial={{ opacity: 0, y: 20 }}
36
+ animate={{ opacity: 1, y: 0 }}
37
+ exit={{ opacity: 0, y: 20 }}
38
+ transition={{ duration: 0.5, ease: "easeOut" }}
39
+ className="fixed bottom-0 left-0 w-full flex justify-center pb-6 pointer-events-none z-[10000]"
40
+ >
41
+ <div className="bg-black/50 backdrop-blur-md border border-white/5 rounded-full px-6 py-2 shadow-2xl">
42
+ <p className="text-white/60 text-xs font-medium tracking-widest uppercase">
43
+ Thanks for spending time here.
44
+ </p>
45
+ </div>
46
+ </motion.div>
47
+ )}
48
+ </AnimatePresence>
49
+ );
50
+ };
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ import { motion, AnimatePresence, useMotionValue, useTransform } from "framer-motion";
4
+ import { X, ZoomIn, ZoomOut, RotateCcw } from "lucide-react";
5
+ import { useEffect, useState, useCallback, useRef } from "react";
6
+ import { createPortal } from "react-dom";
7
+
8
+ interface ImageZoomOverlayProps {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ imageUrl: string;
12
+ altText?: string;
13
+ }
14
+
15
+ export function ImageZoomOverlay({ isOpen, onClose, imageUrl, altText }: ImageZoomOverlayProps) {
16
+ const [mounted, setMounted] = useState(false);
17
+ const [scale, setScale] = useState(1);
18
+ const containerRef = useRef<HTMLDivElement>(null);
19
+
20
+ // Motion values for panning
21
+ const x = useMotionValue(0);
22
+ const y = useMotionValue(0);
23
+
24
+ useEffect(() => {
25
+ setMounted(true);
26
+ }, []);
27
+
28
+ // Reset zoom and pan when opening/closing
29
+ useEffect(() => {
30
+ if (!isOpen) {
31
+ setScale(1);
32
+ x.set(0);
33
+ y.set(0);
34
+ }
35
+ }, [isOpen, x, y]);
36
+
37
+ // Body scroll lock
38
+ useEffect(() => {
39
+ if (isOpen) {
40
+ document.body.style.overflow = 'hidden';
41
+ return () => {
42
+ document.body.style.overflow = '';
43
+ };
44
+ }
45
+ }, [isOpen]);
46
+
47
+ const handleWheel = useCallback((e: React.WheelEvent) => {
48
+ if (e.ctrlKey || e.metaKey) { // OS-level pinch gesture often maps to ctrl+wheel
49
+ e.preventDefault();
50
+ const delta = -e.deltaY * 0.01;
51
+ setScale(prev => Math.min(Math.max(1, prev + delta), 5));
52
+ }
53
+ }, []);
54
+
55
+ const reset = () => {
56
+ setScale(1);
57
+ x.set(0);
58
+ y.set(0);
59
+ };
60
+
61
+ if (!mounted) return null;
62
+
63
+ const content = (
64
+ <AnimatePresence>
65
+ {isOpen && (
66
+ <div
67
+ className="fixed inset-0 z-[999999] flex items-center justify-center overflow-hidden touch-none"
68
+ onWheel={handleWheel}
69
+ >
70
+ {/* Backdrop */}
71
+ <motion.div
72
+ initial={{ opacity: 0 }}
73
+ animate={{ opacity: 1 }}
74
+ exit={{ opacity: 0 }}
75
+ onClick={onClose}
76
+ className="absolute inset-0 bg-black/95 backdrop-blur-xl cursor-zoom-out"
77
+ />
78
+
79
+ {/* Controls HUD */}
80
+ <motion.div
81
+ initial={{ opacity: 0, y: 20 }}
82
+ animate={{ opacity: 1, y: 0 }}
83
+ exit={{ opacity: 0, y: 20 }}
84
+ className="absolute bottom-10 left-1/2 -translate-x-1/2 z-[10] flex items-center gap-4 px-6 py-3 rounded-2xl bg-white/5 border border-white/10 backdrop-blur-md shadow-2xl"
85
+ >
86
+ <button
87
+ onClick={() => setScale(s => Math.max(1, s - 0.5))}
88
+ className="p-2 rounded-xl hover:bg-white/10 text-white/60 hover:text-white transition-all"
89
+ title="Zoom Out"
90
+ >
91
+ <ZoomOut className="w-5 h-5" />
92
+ </button>
93
+
94
+ <div className="w-px h-4 bg-white/10 mx-2" />
95
+
96
+ <span className="text-xs font-mono text-white/40 min-w-[3rem] text-center">
97
+ {Math.round(scale * 100)}%
98
+ </span>
99
+
100
+ <div className="w-px h-4 bg-white/10 mx-2" />
101
+
102
+ <button
103
+ onClick={() => setScale(s => Math.min(5, s + 0.5))}
104
+ className="p-2 rounded-xl hover:bg-white/10 text-white/60 hover:text-white transition-all"
105
+ title="Zoom In"
106
+ >
107
+ <ZoomIn className="w-5 h-5" />
108
+ </button>
109
+
110
+ <button
111
+ onClick={reset}
112
+ className="p-2 rounded-xl hover:bg-white/10 text-white/60 hover:text-white transition-all ml-2"
113
+ title="Reset"
114
+ >
115
+ <RotateCcw className="w-4 h-4" />
116
+ </button>
117
+ </motion.div>
118
+
119
+ {/* Close Button */}
120
+ <motion.button
121
+ initial={{ opacity: 0, scale: 0.5 }}
122
+ animate={{ opacity: 1, scale: 1 }}
123
+ exit={{ opacity: 0, scale: 0.5 }}
124
+ onClick={onClose}
125
+ className="absolute top-8 right-8 z-[10] p-4 rounded-full bg-white/10 hover:bg-white/20 text-white/80 transition-all border border-white/10"
126
+ >
127
+ <X className="w-6 h-6" />
128
+ </motion.button>
129
+
130
+ {/* Hint text */}
131
+ <motion.p
132
+ initial={{ opacity: 0 }}
133
+ animate={{ opacity: 1 }}
134
+ className="absolute top-8 left-1/2 -translate-x-1/2 text-[10px] uppercase tracking-[0.3em] text-white/20 font-black"
135
+ >
136
+ Pinch_or_Scroll_to_Explore
137
+ </motion.p>
138
+
139
+ {/* Image Container */}
140
+ <motion.div
141
+ ref={containerRef}
142
+ className="relative w-full h-full flex items-center justify-center p-4 md:p-20"
143
+ style={{ perspective: 1000 }}
144
+ >
145
+ <motion.img
146
+ src={imageUrl}
147
+ alt={altText || "Zoomed project image"}
148
+ initial={{ opacity: 0, scale: 0.8, y: 20 }}
149
+ animate={{
150
+ opacity: 1,
151
+ scale: scale,
152
+ x: x.get(),
153
+ y: y.get(),
154
+ rotateX: 0,
155
+ transition: {
156
+ opacity: { duration: 0.4 },
157
+ scale: { type: "spring", stiffness: 300, damping: 30 },
158
+ y: { type: "spring", stiffness: 300, damping: 30 }
159
+ }
160
+ }}
161
+ exit={{ opacity: 0, scale: 0.8, y: 20 }}
162
+ drag={scale > 1}
163
+ dragConstraints={containerRef}
164
+ onDrag={(e, info) => {
165
+ x.set(x.get() + info.delta.x);
166
+ y.set(y.get() + info.delta.y);
167
+ }}
168
+ className={`max-w-full max-h-full object-contain shadow-[0_0_100px_rgba(0,0,0,0.5)] rounded-lg md:rounded-2xl transition-shadow duration-500 ${scale > 1 ? 'cursor-grab active:cursor-grabbing' : 'cursor-default'}`}
169
+ draggable={false}
170
+ />
171
+ </motion.div>
172
+ </div>
173
+ )}
174
+ </AnimatePresence>
175
+ );
176
+
177
+ return createPortal(content, document.body);
178
+ }
@@ -0,0 +1,71 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { OTPInput, OTPInputContext } from "input-otp"
5
+ import { Dot } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const InputOTP = React.forwardRef<
10
+ React.ElementRef<typeof OTPInput>,
11
+ React.ComponentPropsWithoutRef<typeof OTPInput>
12
+ >(({ className, containerClassName, ...props }, ref) => (
13
+ <OTPInput
14
+ ref={ref}
15
+ containerClassName={cn(
16
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
17
+ containerClassName
18
+ )}
19
+ className={cn("disabled:cursor-not-allowed", className)}
20
+ {...props}
21
+ />
22
+ ))
23
+ InputOTP.displayName = "InputOTP"
24
+
25
+ const InputOTPGroup = React.forwardRef<
26
+ React.ElementRef<"div">,
27
+ React.ComponentPropsWithoutRef<"div">
28
+ >(({ className, ...props }, ref) => (
29
+ <div ref={ref} className={cn("flex items-center", className)} {...props} />
30
+ ))
31
+ InputOTPGroup.displayName = "InputOTPGroup"
32
+
33
+ const InputOTPSlot = React.forwardRef<
34
+ React.ElementRef<"div">,
35
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
36
+ >(({ index, className, ...props }, ref) => {
37
+ const inputOTPContext = React.useContext(OTPInputContext)
38
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
39
+
40
+ return (
41
+ <div
42
+ ref={ref}
43
+ className={cn(
44
+ "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
45
+ isActive && "z-10 ring-2 ring-ring ring-offset-background",
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ {char}
51
+ {hasFakeCaret && (
52
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
53
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
54
+ </div>
55
+ )}
56
+ </div>
57
+ )
58
+ })
59
+ InputOTPSlot.displayName = "InputOTPSlot"
60
+
61
+ const InputOTPSeparator = React.forwardRef<
62
+ React.ElementRef<"div">,
63
+ React.ComponentPropsWithoutRef<"div">
64
+ >(({ ...props }, ref) => (
65
+ <div ref={ref} role="separator" {...props}>
66
+ <Dot />
67
+ </div>
68
+ ))
69
+ InputOTPSeparator.displayName = "InputOTPSeparator"
70
+
71
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }