import { useState, type ReactNode } from 'react' import { motion, AnimatePresence, LayoutGroup, type PanInfo } from 'framer-motion' import { cn } from '../../lib/utils' import { Grid3X3, Layers, LayoutList } from 'lucide-react' export type LayoutMode = 'stack' | 'grid' | 'list' export interface CardData { id: string title: string description: string icon?: ReactNode color?: string } export interface MorphingCardStackProps { cards?: CardData[] className?: string defaultLayout?: LayoutMode onCardClick?: (card: CardData) => void } const layoutIcons = { stack: Layers, grid: Grid3X3, list: LayoutList, } const SWIPE_THRESHOLD = 50 export function Component({ cards = [], className, defaultLayout = 'stack', onCardClick, }: MorphingCardStackProps) { const [layout, setLayout] = useState(defaultLayout) const [expandedCard, setExpandedCard] = useState(null) const [activeIndex, setActiveIndex] = useState(0) const [isDragging, setIsDragging] = useState(false) if (!cards || cards.length === 0) { return null } const handleDragEnd = ( _event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo, ) => { const { offset, velocity } = info const swipe = Math.abs(offset.x) * velocity.x if (offset.x < -SWIPE_THRESHOLD || swipe < -1000) { // Swiped left - go to next card setActiveIndex((prev) => (prev + 1) % cards.length) } else if (offset.x > SWIPE_THRESHOLD || swipe > 1000) { // Swiped right - go to previous card setActiveIndex((prev) => (prev - 1 + cards.length) % cards.length) } setIsDragging(false) } const getStackOrder = () => { const reordered = [] for (let i = 0; i < cards.length; i++) { const index = (activeIndex + i) % cards.length reordered.push({ ...cards[index], stackPosition: i }) } return reordered.reverse() // Reverse so top card renders last (on top) } const getLayoutStyles = (stackPosition: number) => { switch (layout) { case 'stack': return { top: stackPosition * 8, left: stackPosition * 8, zIndex: cards.length - stackPosition, rotate: (stackPosition - 1) * 2, } case 'grid': return { top: 0, left: 0, zIndex: 1, rotate: 0, } case 'list': return { top: 0, left: 0, zIndex: 1, rotate: 0, } } } const containerStyles = { stack: 'relative h-[380px] w-[320px] sm:w-[380px]', grid: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full max-w-[1100px]', list: 'flex flex-col gap-4 w-full max-w-3xl', } const displayCards = layout === 'stack' ? getStackOrder() : cards.map((c, i) => ({ ...c, stackPosition: i })) return (
{/* Layout Toggle */}
{(Object.keys(layoutIcons) as LayoutMode[]).map((mode) => { const Icon = layoutIcons[mode] return ( ) })}
{/* Cards Container */} {displayCards.map((card) => { const styles = getLayoutStyles(card.stackPosition) const isExpanded = expandedCard === card.id const isTopCard = layout === 'stack' && card.stackPosition === 0 return ( setIsDragging(true)} onDragEnd={handleDragEnd} whileDrag={{ scale: 1.02, cursor: 'grabbing' }} onClick={() => { if (isDragging) return setExpandedCard(isExpanded ? null : card.id) onCardClick?.(card) }} className={cn( 'cursor-pointer rounded-2xl overflow-hidden transition-colors', layout === 'stack' && 'absolute w-full h-full p-8', layout === 'stack' && isTopCard && 'cursor-grab active:cursor-grabbing', layout === 'grid' && 'p-8', layout === 'list' && 'p-6 md:p-8', )} style={{ backgroundColor: 'var(--lp-bg)', border: isExpanded ? '1px solid var(--lp-text)' : '1px solid var(--lp-line)', boxShadow: layout === 'stack' ? '0 10px 40px -10px rgba(0,0,0,0.2)' : 'none', }} >
{card.icon && (
{card.icon}
)}

{card.title}

{card.description}

{isTopCard && (
Swipe to navigate
)}
) })}
{layout === 'stack' && cards.length > 1 && (
{cards.map((_, index) => (
)}
) }