"use client"; import { useId, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode } from "react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; type AccordionMorphProps = { /** Rows, top to bottom. */ items: { title: string; content: ReactNode }[]; /** Let more than one row stay open. */ multiple?: boolean; /** Row numbers open at the start, from 0. */ defaultOpen?: number[]; /** Open row tint and icon colour. */ accent?: string; /** Replaces the row look. Keep corners out of it, they are the morph. */ className?: string; }; /* AccordionMorph: each row is a pill that morphs into a tinted card as it opens, while its plus folds into a minus. */ export function AccordionMorph({ items, multiple = false, defaultOpen = [], accent = "#a78bfa", className = "border border-white/10 bg-white/5 text-white" }: AccordionMorphProps) { const reduce = useReducedMotion(); const id = useId(); const root = useRef(null); const [open, setOpen] = useState(defaultOpen); const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 320, damping: 32 }; const toggle = (i: number) => setOpen((now) => (now.includes(i) ? now.filter((row) => row !== i) : multiple ? [...now, i] : [i])); const onKey = (e: KeyboardEvent, i: number) => { const last = items.length - 1; const moves: Record = { ArrowDown: i === last ? 0 : i + 1, ArrowUp: i === 0 ? last : i - 1, Home: 0, End: last }; if (!(e.key in moves)) return; e.preventDefault(); root.current?.querySelectorAll("[data-accordion-row]")[moves[e.key]]?.focus(); }; return (
{items.map((item, i) => { const isOpen = open.includes(i); return (

{isOpen && (
{item.content}
)}
); })}
); }