"use client"; import { useId, useRef, useState, type KeyboardEvent, type ReactNode } from "react"; import { motion, useReducedMotion } from "motion/react"; export type ExpandableGridItem = { id: string; title: string; summary?: string; /** Image at the top of the tile. It sits inside a button, so no links here. */ media?: ReactNode; /** Shown when the tile is open. Links and buttons are fine here. */ content: ReactNode; }; type ExpandableGridProps = { items: ExpandableGridItem[]; /** id of the tile open at the start. */ start?: string | null; /** Smallest tile width in pixels. The grid fits as many columns as it can. */ minWidth?: number; /** Look of each tile. */ cardClassName?: string; /** On the grid. Set the gap and width here. */ className?: string; }; const SPRING = { type: "spring", stiffness: 320, damping: 34 } as const; /* ExpandableGrid: open a tile and it takes the full row while the others slide around it. Open one at a time, Esc closes. */ export function ExpandableGrid({ items, start = null, minWidth = 200, cardClassName = "border border-white/10 bg-white/[0.04] text-white", className = "gap-3", }: ExpandableGridProps) { const reduce = useReducedMotion(); const uid = useId(); const [openId, setOpenId] = useState(start); const openPanel = useRef(null); const transition = reduce ? { duration: 0 } : SPRING; const closeOnEsc = (e: KeyboardEvent) => { if (e.key !== "Escape") return; setOpenId(null); e.currentTarget.querySelector("button")?.focus(); }; // Waits for the tile to settle, because the box measured mid-animation is not its real size. const showOpenCard = () => openPanel.current?.closest("li")?.scrollIntoView({ block: "nearest", behavior: reduce ? "auto" : "smooth" }); return ( ); }