/** * App Store Card Expansion * * iOS App Store-style card that expands to full screen with shared * layout animation. Multiple elements animate together. * * Key techniques: * - Multiple layoutId elements that animate together * - whileTap for press feedback * - Overlay with separate AnimatePresence * - useOnClickOutside and Escape key for dismiss * - borderRadius in style prop to prevent distortion */ "use client"; import { useState, useEffect, useRef } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { useOnClickOutside } from "usehooks-ts"; type Card = { title: string; description: string; longDescription: string; image: string; }; // Card in grid view function Card({ card, setActiveCard, }: { card: Card; setActiveCard: (card: Card | null) => void; }) { return ( setActiveCard(card)} style={{ borderRadius: 20 }} > {card.title} {card.description} {/* Hidden long description - will animate in when expanded */} {card.longDescription} ); } // Expanded card view function ActiveCard({ activeCard, setActiveCard, }: { activeCard: Card; setActiveCard: (card: Card | null) => void; }) { const ref = useRef(null); useOnClickOutside(ref, () => setActiveCard(null)); return ( setActiveCard(null)} > ✕ {activeCard.title} {activeCard.description} {/* Long description now visible */} {activeCard.longDescription} ); } export default function AppStoreCards() { const [activeCard, setActiveCard] = useState(null); // Dismiss on Escape useEffect(() => { function onKeyDown(event: KeyboardEvent) { if (event.key === "Escape") setActiveCard(null); } window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, []); return (
{CARDS.map((card) => ( ))} {/* Overlay */} {activeCard && ( )} {/* Expanded card */} {activeCard && ( )}
); } const CARDS: Card[] = [ { title: "Game Title", description: "A brief description", longDescription: "Full description with more details about the game...", image: "/game-image.webp", }, ];