Expandable Card primitive to easily condense and expand details.
1"use client"
2
3import React, { useState } from "react"
4import {
5 Battery,
6 Bluetooth,
7 Calendar,
8 Clock,
9 Cloud,
10 Droplets,
11 Fingerprint,
12 MapPin,
13 MessageSquare,
14 Mic,
15 ShoppingCart,
16 Star,
17 Sun,
18 Users,
19 Video,
20 Wind,
21} from "lucide-react"
22import { AnimatePresence, motion } from "motion/react"
23import { toast } from "sonner"
24
25import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
26import { Badge } from "@/components/ui/badge"
27import { Button } from "@/components/ui/button"
28import {
29 Tooltip,
30 TooltipContent,
31 TooltipProvider,
32 TooltipTrigger,
33} from "@/components/ui/tooltip"
34import {
35 Expandable,
36 ExpandableCard,
37 ExpandableCardContent,
38 ExpandableCardFooter,
39 ExpandableCardHeader,
40 ExpandableContent,
41 ExpandableTrigger,
42} from "@/components/ui/expandable"
43
44// _____________________EXAMPLES______________________
45function DesignSyncExample() {
46 return (
47 <Expandable
48 expandDirection="both"
49 expandBehavior="replace"
50 initialDelay={0.2}
51 onExpandStart={() => console.log("Expanding meeting card...")}
52 onExpandEnd={() => console.log("Meeting card expanded!")}
53 >
54 {({ isExpanded }) => (
55 <ExpandableTrigger>
56 <ExpandableCard
57 className="w-full relative"
58 collapsedSize={{ width: 320, height: 240 }}
59 expandedSize={{ width: 420, height: 480 }}
60 hoverToExpand={false}
61 expandDelay={200}
62 collapseDelay={500}
63 >
64 <ExpandableCardHeader>
65 <div className="flex justify-between items-start w-full">
66 <div>
67 <Badge
68 variant="secondary"
69 className="bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-100 mb-2"
70 >
71 In 15 mins
72 </Badge>
73 <h3 className="font-semibold text-xl text-gray-800 dark:text-white">
74 Design Sync
75 </h3>
76 </div>
77 <TooltipProvider>
78 <Tooltip>
79 <TooltipTrigger asChild>
80 <Button size="icon" variant="outline" className="h-8 w-8">
81 <Calendar className="h-4 w-4" />
82 </Button>
83 </TooltipTrigger>
84 <TooltipContent>
85 <p>Add to Calendar</p>
86 </TooltipContent>
87 </Tooltip>
88 </TooltipProvider>
89 </div>
90 </ExpandableCardHeader>
91
92 <ExpandableCardContent>
93 <div className="flex flex-col items-start justify-between mb-4">
94 <div className="flex items-center text-sm text-gray-600 dark:text-gray-300">
95 <Clock className="h-4 w-4 mr-1" />
96 <span>1:30PM → 2:30PM</span>
97 </div>
98
99 <ExpandableContent preset="blur-md">
100 <div className="flex items-center text-sm text-gray-600 dark:text-gray-300">
101 <MapPin className="h-4 w-4 mr-1" />
102 <span>Conference Room A</span>
103 </div>
104 </ExpandableContent>
105 </div>
106 <ExpandableContent preset="blur-md" stagger staggerChildren={0.2}>
107 <p className="text-sm text-gray-700 dark:text-gray-200 mb-4">
108 Weekly design sync to discuss ongoing projects, share updates,
109 and address any design-related challenges.
110 </p>
111 <div className="mb-4">
112 <h4 className="font-medium text-sm text-gray-800 dark:text-gray-100 mb-2 flex items-center">
113 <Users className="h-4 w-4 mr-2" />
114 Attendees:
115 </h4>
116 <div className="flex -space-x-2 overflow-hidden">
117 {["Alice", "Bob", "Charlie", "David"].map((name, index) => (
118 <TooltipProvider key={index}>
119 <Tooltip>
120 <TooltipTrigger asChild>
121 <Avatar className="border-2 border-white dark:border-gray-800">
122 <AvatarImage
123 src={`/placeholder.svg?height=32&width=32&text=${name[0]}`}
124 alt={name}
125 />
126 <AvatarFallback>{name[0]}</AvatarFallback>
127 </Avatar>
128 </TooltipTrigger>
129 <TooltipContent>
130 <p>{name}</p>
131 </TooltipContent>
132 </Tooltip>
133 </TooltipProvider>
134 ))}
135 </div>
136 </div>
137 <div className="space-y-2">
138 <Button className="w-full bg-red-600 hover:bg-red-700 text-white">
139 <Video className="h-4 w-4 mr-2" />
140 Join Meeting
141 </Button>
142 {isExpanded && (
143 <Button variant="outline" className="w-full">
144 <MessageSquare className="h-4 w-4 mr-2" />
145 Open Chat
146 </Button>
147 )}
148 </div>
149 </ExpandableContent>
150 </ExpandableCardContent>
151 <ExpandableContent preset="slide-up">
152 <ExpandableCardFooter>
153 <div className="flex items-center justify-between w-full text-sm text-gray-600 dark:text-gray-300">
154 <span>Weekly</span>
155 <span>Next: Mon, 10:00 AM</span>
156 </div>
157 </ExpandableCardFooter>
158 </ExpandableContent>
159 </ExpandableCard>
160 </ExpandableTrigger>
161 )}
162 </Expandable>
163 )
164}
165
166export function ProductShowcaseCard() {
167 return (
168 <Expandable
169 expandDirection="both"
170 expandBehavior="replace"
171 onExpandStart={() => console.log("Expanding product card...")}
172 onExpandEnd={() => console.log("Product card expanded!")}
173 >
174 {({ isExpanded }) => (
175 <ExpandableTrigger>
176 <ExpandableCard
177 className="w-full relative"
178 collapsedSize={{ width: 330, height: 220 }}
179 expandedSize={{ width: 500, height: 520 }}
180 hoverToExpand={false}
181 expandDelay={500}
182 collapseDelay={700}
183 >
184 <ExpandableCardHeader>
185 <div className="flex justify-between items-center">
186 <Badge
187 variant="secondary"
188 className="bg-blue-100 text-blue-800"
189 >
190 New Arrival
191 </Badge>
192 <Badge variant="outline" className="ml-2">
193 $129.99
194 </Badge>
195 </div>
196 </ExpandableCardHeader>
197
198 <ExpandableCardContent>
199 <div className="flex items-start mb-4">
200 <img
201 src="https://pisces.bbystatic.com/image2/BestBuy_US/images/products/6505/6505727_rd.jpg;maxHeight=640;maxWidth=550;format=webp"
202 alt="Product"
203 className="object-cover rounded-md mr-4"
204 style={{
205 width: isExpanded ? "120px" : "80px",
206 height: isExpanded ? "120px" : "80px",
207 transition: "width 0.3s, height 0.3s",
208 }}
209 />
210 <div className="flex-1">
211 <h3
212 className="font-medium text-gray-800 dark:text-white tracking-tight transition-all duration-300"
213 style={{
214 fontSize: isExpanded ? "24px" : "18px",
215 fontWeight: isExpanded ? "700" : "400",
216 }}
217 >
218 Sony Headphones
219 </h3>
220 <div className="flex items-center mt-1">
221 {[1, 2, 3, 4, 5].map((star) => (
222 <Star
223 key={star}
224 className="w-4 h-4 text-yellow-400 fill-current"
225 />
226 ))}
227 <AnimatePresence mode="wait">
228 {isExpanded ? (
229 <motion.span
230 key="expanded"
231 initial={{ opacity: 0, width: 0 }}
232 animate={{ opacity: 1, width: "auto" }}
233 exit={{ opacity: 0, width: 0 }}
234 transition={{ duration: 0.2 }}
235 className="ml-2 text-sm text-gray-600 dark:text-gray-400 overflow-hidden whitespace-nowrap"
236 >
237 (128 reviews)
238 </motion.span>
239 ) : (
240 <motion.span
241 key="collapsed"
242 initial={{ opacity: 0, width: 0 }}
243 animate={{ opacity: 1, width: "auto" }}
244 exit={{ opacity: 0, width: 0 }}
245 transition={{ duration: 0.2 }}
246 className="ml-2 text-sm text-gray-600 dark:text-gray-400 overflow-hidden whitespace-nowrap"
247 >
248 (128)
249 </motion.span>
250 )}
251 </AnimatePresence>
252 </div>
253 </div>
254 </div>
255 <ExpandableContent
256 preset="fade"
257 keepMounted={false}
258 animateIn={{
259 initial: { opacity: 0, y: 20 },
260 animate: { opacity: 1, y: 0 },
261 transition: { type: "spring", stiffness: 300, damping: 20 },
262 }}
263 >
264 <p className="text-sm text-gray-600 dark:text-gray-400 mb-4 max-w-xs">
265 Experience crystal-clear audio with our latest
266 noise-cancelling technology. Perfect for work, travel, or
267 relaxation.
268 </p>
269
270 <div className="space-y-4">
271 {[
272 { icon: Battery, text: "30-hour battery life" },
273 { icon: Bluetooth, text: "Bluetooth 5.0" },
274 { icon: Fingerprint, text: "Touch controls" },
275 { icon: Mic, text: "Voice assistant compatible" },
276 ].map((feature, index) => (
277 <div
278 key={index}
279 className="flex items-center text-sm text-gray-600 dark:text-gray-400"
280 >
281 <feature.icon className="w-4 h-4 mr-2" />
282 <span>{feature.text}</span>
283 </div>
284 ))}
285
286 <Button className="w-full bg-blue-600 hover:bg-blue-700 text-white">
287 <ShoppingCart className="w-4 h-4 mr-2" />
288 Add to Cart
289 </Button>
290 </div>
291 </ExpandableContent>
292 </ExpandableCardContent>
293 <ExpandableContent preset="slide-up">
294 <ExpandableCardFooter>
295 <div className="flex justify-between text-sm text-gray-600 dark:text-gray-400 w-full">
296 <span>Free shipping</span>
297 <span>30-day return policy</span>
298 </div>
299 </ExpandableCardFooter>
300 </ExpandableContent>
301 </ExpandableCard>
302 </ExpandableTrigger>
303 )}
304 </Expandable>
305 )
306}
307
308export function WeatherForecastCard() {
309 return (
310 <Expandable expandDirection="both" expandBehavior="replace">
311 <ExpandableTrigger>
312 <ExpandableCard
313 collapsedSize={{ width: 300, height: 220 }}
314 expandedSize={{ width: 500, height: 420 }}
315 hoverToExpand={false}
316 expandDelay={100}
317 collapseDelay={400}
318 >
319 <ExpandableCardHeader>
320 <div className="flex items-center justify-between">
321 <div className="flex items-center">
322 <Sun className="w-8 h-8 text-yellow-400 mr-2" />
323 <ExpandableContent preset="blur-sm" keepMounted={true}>
324 <h3 className="font-medium text-lg">Today's Weather</h3>
325 <Badge
326 variant="secondary"
327 className="bg-blue-100 text-blue-800"
328 >
329 72°F
330 </Badge>
331 </ExpandableContent>
332 </div>
333 </div>
334 </ExpandableCardHeader>
335
336 <ExpandableCardContent>
337 <div className="flex justify-between items-center mb-4">
338 <div>
339 <p className="text-2xl font-bold">72°F</p>
340 <p className="text-sm text-gray-600 dark:text-gray-400">
341 Feels like 75°F
342 </p>
343 </div>
344 <div className="text-right">
345 <p className="font-medium">Sunny</p>
346 <ExpandableContent
347 preset="blur-sm"
348 stagger
349 staggerChildren={0.1}
350 keepMounted={true}
351 animateIn={{
352 initial: { opacity: 0, y: 20, rotate: -5 },
353 animate: { opacity: 1, y: 0, rotate: 0 },
354 transition: { type: "spring", stiffness: 300, damping: 20 },
355 }}
356 >
357 <p className="text-sm text-gray-600 dark:text-gray-400">
358 High 78° / Low 65°
359 </p>
360 </ExpandableContent>
361 </div>
362 </div>
363 <ExpandableContent
364 preset="blur-sm"
365 stagger
366 staggerChildren={0.1}
367 keepMounted={true}
368 animateIn={{
369 initial: { opacity: 0, y: 20, rotate: -5 },
370 animate: { opacity: 1, y: 0, rotate: 0 },
371 transition: { type: "spring", stiffness: 300, damping: 20 },
372 }}
373 >
374 <div className="space-y-2 mb-4">
375 <div className="flex justify-between items-center">
376 <div className="flex items-center">
377 <Cloud className="w-5 h-5 mr-2 text-gray-400" />
378 <span>Humidity</span>
379 </div>
380 <span>45%</span>
381 </div>
382 <div className="flex justify-between items-center">
383 <div className="flex items-center">
384 <Wind className="w-5 h-5 mr-2 text-gray-400" />
385 <span>Wind</span>
386 </div>
387 <span>8 mph</span>
388 </div>
389 <div className="flex justify-between items-center">
390 <div className="flex items-center">
391 <Droplets className="w-5 h-5 mr-2 text-gray-400" />
392 <span>Precipitation</span>
393 </div>
394 <span>0%</span>
395 </div>
396 </div>
397 <div className="space-y-2">
398 <h4 className="font-medium">5-Day Forecast</h4>
399 {["Mon", "Tue", "Wed", "Thu", "Fri"].map((day, index) => (
400 <div key={day} className="flex justify-between items-center">
401 <span>{day}</span>
402 <div className="flex items-center">
403 <Sun className="w-4 h-4 text-yellow-400 mr-2" />
404 <span>{70 + index}°F</span>
405 </div>
406 </div>
407 ))}
408 </div>
409 </ExpandableContent>
410 </ExpandableCardContent>
411 <ExpandableCardFooter>
412 <p className="text-xs text-gray-500 dark:text-gray-400">
413 Last updated: 5 minutes ago
414 </p>
415 </ExpandableCardFooter>
416 </ExpandableCard>
417 </ExpandableTrigger>
418 </Expandable>
419 )
420}
421
422function ControlledExpandableCard() {
423 const [isExpanded, setIsExpanded] = useState(false)
424
425 const handleToggle = () => {
426 setIsExpanded((prev) => !prev)
427 }
428
429 return (
430 <div className="space-y-4">
431 <Button onClick={handleToggle} className="mb-4">
432 {isExpanded ? "Collapse" : "Expand"}
433 </Button>
434
435 <Expandable
436 expanded={isExpanded}
437 onToggle={handleToggle}
438 expandDirection="vertical"
439 expandBehavior="push"
440 onExpandStart={() => toast.info("Expanding controlled card...")}
441 onExpandEnd={() => toast.info("Controlled card expanded!")}
442 >
443 <ExpandableCard
444 collapsedSize={{ width: 300, height: 100 }}
445 expandedSize={{ width: 300, height: 300 }}
446 >
447 <ExpandableTrigger>
448 <ExpandableCardHeader>
449 <h3 className="text-lg font-semibold">
450 Controlled Expandable Card
451 </h3>
452 <Badge variant="secondary">
453 {isExpanded ? "Expanded" : "Collapsed"}
454 </Badge>
455 </ExpandableCardHeader>
456 </ExpandableTrigger>
457 <ExpandableCardContent>
458 <p className="mb-4">
459 This card's expanded state is controlled externally.
460 </p>
461 <ExpandableContent preset="fade" stagger staggerChildren={0.1}>
462 <p className="mb-2">This content fades in when expanded.</p>
463 <p className="mb-2">
464 It uses staggered animation for child elements.
465 </p>
466 <p>The expansion is controlled by the button above.</p>
467 </ExpandableContent>
468 </ExpandableCardContent>
469 <ExpandableCardFooter>
470 <ExpandableContent preset="slide-up">
471 <p className="text-sm text-gray-500">
472 Footer content slides up when expanded
473 </p>
474 </ExpandableContent>
475 </ExpandableCardFooter>
476 </ExpandableCard>
477 </Expandable>
478 </div>
479 )
480}
481
482export function ExpandableCardExamples() {
483 return (
484 <div className="p-8 w-full max-w-7xl mx-auto space-y-12">
485 <div className="flex flex-col items-center space-y-24">
486 <div className="min-h-[480px]">
487 <DesignSyncExample />
488 </div>
489 <div className="flex gap-24 min-h-[600px]">
490 <ProductShowcaseCard />
491 <WeatherForecastCard />
492 </div>
493 {/* <div>
494 </div> */}
495 {/* <div>
496 <h2 className="text-xl font-semibold mb-4">Controlled Expandable</h2>
497 <ControlledExpandableCard />
498 </div> */}
499 </div>
500 </div>
501 )
502}