lightbox-demo.tsx
styles.css
prompt.md
Collapse
Reset
Copy Code
Download
Run
1
"use client";
2
3
import { useState, useEffect, useCallback } from "react";
4
import { motion, AnimatePresence } from "framer-motion";
5
import { X, Expand, ChevronLeft, ChevronRight } from "lucide-react";
6
import "@/components/ui/lightbox.css";
7
8
const ITEMS = [
9
{
10
src: "https://images.unsplash.com/photo-1494438639946-1ebd1d20bf85?q=80&w=1000&auto=format&fit=crop",
11
className: "md:col-span-2 md:row-span-2",
12
},
13
{
14
src: "https://images.unsplash.com/photo-1492144534655-ae79c964c9d7?q=80&w=1000&auto=format&fit=crop",
15
className: "md:col-span-1 md:row-span-1",
16
},
17
{
18
src: "https://images.unsplash.com/photo-1481026469463-66327c86e544?q=80&w=1000&auto=format&fit=crop",
19
className: "md:col-span-1 md:row-span-2",
20
},
21
{
22
src: "https://images.unsplash.com/photo-1449844908441-8829872d2607?q=80&w=1000&auto=format&fit=crop",
23
className: "md:col-span-1 md:row-span-1",
24
},
25
{
26
src: "https://images.unsplash.com/photo-1518098268026-4e89f1a2cd8e?q=80&w=1000&auto=format&fit=crop",
27
className: "md:col-span-2 md:row-span-1",
28
},
29
{
30
src: "https://images.unsplash.com/photo-1513694203232-719a280e022f?q=80&w=1000&auto=format&fit=crop",
31
className: "md:col-span-1 md:row-span-1",
32
},
33
{
34
src: "https://images.unsplash.com/photo-1469474968028-56623f02e42e?q=80&w=1000&auto=format&fit=crop",
35
className: "md:col-span-1 md:row-span-1",
36
},
37
{
38
src: "https://images.unsplash.com/photo-1473448912268-2022ce9509d8?q=80&w=1000&auto=format&fit=crop",
39
className: "md:col-span-1 md:row-span-2",
40
},
41
{
42
src: "https://images.unsplash.com/photo-1472214103451-9374bd1c798e?q=80&w=1000&auto=format&fit=crop",
43
className: "md:col-span-2 md:row-span-1",
44
},
45
{
46
src: "https://images.unsplash.com/photo-1522204523234-8729aa6e3d5f?q=80&w=1000&auto=format&fit=crop",
47
className: "md:col-span-1 md:row-span-1",
48
},
49
{
50
src: "https://images.unsplash.com/photo-1497366216548-37526070297c?q=80&w=1000&auto=format&fit=crop",
51
className: "md:col-span-2 md:row-span-2",
52
},
53
{
54
src: "https://images.unsplash.com/photo-1493246507139-91e8fad9978e?q=80&w=1000&auto=format&fit=crop",
55
className: "md:col-span-1 md:row-span-1",
56
},
57
{
58
src: "https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1000&auto=format&fit=crop",
59
className: "md:col-span-1 md:row-span-1",
60
},
61
{
62
src: "https://images.unsplash.com/photo-1511497584788-876760111969?q=80&w=1000&auto=format&fit=crop",
63
className: "md:col-span-1 md:row-span-1",
64
}
65
];
66
67
export function LightboxDemo() {
68
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
69
70
const handleNext = useCallback(() => {
71
if (selectedIndex !== null) {
72
setSelectedIndex((prev) => (prev! + 1) % ITEMS.length);
73
}
74
}, [selectedIndex]);
75
76
const handlePrev = useCallback(() => {
77
if (selectedIndex !== null) {
78
setSelectedIndex((prev) => (prev! - 1 + ITEMS.length) % ITEMS.length);
79
}
80
}, [selectedIndex]);
81
82
const handleClose = useCallback(() => {
83
setSelectedIndex(null);
84
}, []);
85
86
useEffect(() => {
87
const handleKeyDown = (e: KeyboardEvent) => {
88
if (selectedIndex === null) return;
89
if (e.key === "ArrowRight") handleNext();
90
if (e.key === "ArrowLeft") handlePrev();
91
if (e.key === "Escape") handleClose();
92
};
93
window.addEventListener("keydown", handleKeyDown);
94
return () => window.removeEventListener("keydown", handleKeyDown);
95
}, [selectedIndex, handleNext, handlePrev, handleClose]);
96
97
// Lock body scroll when modal is open
98
useEffect(() => {
99
if (selectedIndex !== null) {
100
document.body.style.overflow = "hidden";
101
} else {
102
document.body.style.overflow = "auto";
103
}
104
return () => {
105
document.body.style.overflow = "auto";
106
};
107
}, [selectedIndex]);
108
109
return (
110
<div className="w-full h-full p-4 sm:p-6 lg:p-8 overflow-y-auto">
111
{/* Bento Grid Layout */}
112
<div className="grid grid-cols-1 md:grid-cols-4 auto-rows-[240px] md:auto-rows-[200px] gap-4 max-w-5xl mx-auto pb-20">
113
{ITEMS.map((item, idx) => (
114
<motion.div
115
key={idx}
116
layoutId={`lightbox-image-${idx}`}
117
className={`relative overflow-hidden rounded-[20px] cursor-zoom-in group shadow-[0_4px_20px_rgba(0,0,0,0.05)] dark:shadow-[0_4px_20px_rgba(255,255,255,0.02)] border border-black/5 dark:border-white/5 ${item.className}`}
118
onClick={() => setSelectedIndex(idx)}
119
whileHover={{ scale: 0.985 }}
120
whileTap={{ scale: 0.95 }}
121
transition={{ type: "spring", stiffness: 400, damping: 25 }}
122
>
123
<img
124
src={item.src}
125
alt={`Creative collage image ${idx + 1}`}
126
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
127
loading="lazy"
128
/>
129
{/* Elegant overlay */}
130
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 dark:group-hover:bg-white/10 transition-colors duration-500" />
131
132
{/* Expand Icon */}
133
<div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
134
<div className="bg-white/20 backdrop-blur-md p-2 rounded-full text-white shadow-lg">
135
<Expand className="w-4 h-4" />
136
</div>
137
</div>
138
</motion.div>
139
))}
140
</div>
141
142
{/* Lightbox Modal */}
143
<AnimatePresence>
144
{selectedIndex !== null && (
145
<motion.div
146
initial={{ opacity: 0 }}
147
animate={{ opacity: 1 }}
148
exit={{ opacity: 0 }}
149
transition={{ duration: 0.3 }}
150
className="fixed inset-0 z-50 flex items-center justify-center bg-white/95 dark:bg-black/95 backdrop-blur-xl p-4 sm:p-8"
151
onClick={handleClose}
152
>
153
{/* Image Wrapper */}
154
<motion.div
155
layoutId={`lightbox-image-${selectedIndex}`}
156
className="relative max-w-6xl w-full max-h-[90vh] rounded-3xl overflow-hidden shadow-[0_20px_50px_rgba(0,0,0,0.3)] flex items-center justify-center group"
157
onClick={(e) => e.stopPropagation()}
158
>
159
<img
160
src={ITEMS[selectedIndex].src}
161
alt="Selected full screen"
162
className="w-full h-full max-h-[90vh] object-contain rounded-3xl"
163
/>
164
165
{/* Controls Overlay */}
166
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none flex items-center justify-between p-6">
167
168
{/* Previous Button */}
169
<button
170
onClick={(e) => { e.stopPropagation(); handlePrev(); }}
171
className="pointer-events-auto w-12 h-12 flex items-center justify-center rounded-full bg-black/40 hover:bg-black/60 text-white backdrop-blur-xl transition-all hover:scale-105 active:scale-95 border border-white/20 shadow-xl"
172
>
173
<ChevronLeft className="w-6 h-6" />
174
</button>
175
176
{/* Next Button */}
177
<button
178
onClick={(e) => { e.stopPropagation(); handleNext(); }}
179
className="pointer-events-auto w-12 h-12 flex items-center justify-center rounded-full bg-black/40 hover:bg-black/60 text-white backdrop-blur-xl transition-all hover:scale-105 active:scale-95 border border-white/20 shadow-xl"
180
>
181
<ChevronRight className="w-6 h-6" />
182
</button>
183
</div>
184
185
{/* Close Button */}
186
<button
187
onClick={handleClose}
188
className="absolute top-6 right-6 w-12 h-12 flex items-center justify-center rounded-full bg-black/40 hover:bg-black/60 text-white backdrop-blur-xl transition-all hover:scale-105 active:scale-95 border border-white/20 shadow-xl pointer-events-auto"
189
>
190
<X className="w-6 h-6" />
191
</button>
192
</motion.div>
193
194
{/* Bottom Counter */}
195
<motion.div
196
initial={{ opacity: 0, y: 20 }}
197
animate={{ opacity: 1, y: 0 }}
198
exit={{ opacity: 0, y: 20 }}
199
className="absolute bottom-8 text-black dark:text-white font-medium tracking-wide bg-white/50 dark:bg-black/50 backdrop-blur-md px-4 py-1.5 rounded-full"
200
>
201
{selectedIndex + 1} / {ITEMS.length}
202
</motion.div>
203
</motion.div>
204
)}
205
</AnimatePresence>
206
</div>
207
);
208
}
209

