/**
* 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",
},
];