"use client"; import { useEffect, useId, useRef, useState, type ElementType, type KeyboardEvent, type ReactNode } from "react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; export type FAQMorphItem = { question: string; answer: ReactNode; }; type FAQMorphProps = { items: FAQMorphItem[]; title?: string; /** Small line above the title. */ eyebrow?: string; subtitle?: string; /** Which question starts open. */ defaultOpen?: number; /** Colour of the marker and the answer panel. */ tile?: string; /** Colour of the number in the answer panel. */ color?: string; /** h2 by default. */ headingAs?: ElementType; /** Set padding and colours here. */ className?: string; }; const SPRING = { type: "spring", stiffness: 380, damping: 34 } as const; /* FAQMorph: questions on one side, one answer panel on the other. The marker slides to the question and the panel reshapes to fit the answer. Narrow, it turns into a plain accordion. */ export function FAQMorph({ items, title, eyebrow, subtitle, defaultOpen = 0, tile = "#15122a", color = "#a78bfa", headingAs: Heading = "h2", className = "bg-[#0b0a14] px-5 py-16 text-white", }: FAQMorphProps) { const reduce = useReducedMotion(); const id = useId(); const side = useRef(null); const inner = useRef(null); const [open, setOpen] = useState(defaultOpen); const [shown, setShown] = useState(defaultOpen); const [height, setHeight] = useState(); const answer = items[shown]; useEffect(() => { if (!inner.current) return; const watcher = new ResizeObserver(([entry]) => { const next = entry.target.getBoundingClientRect().height; if (next) setHeight(next); }); watcher.observe(inner.current); return () => watcher.disconnect(); }, []); const pick = (i: number) => { // With the side panel showing there is always one answer; only the narrow accordion can close. if (open === i && !side.current?.offsetWidth) return setOpen(null); setOpen(i); setShown(i); }; const step = (e: KeyboardEvent) => { if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return; const buttons = Array.from(e.currentTarget.querySelectorAll("button")); const at = buttons.indexOf(e.target as HTMLButtonElement); if (at < 0) return; e.preventDefault(); buttons[(at + (e.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length].focus(); }; return (
{title && (
{eyebrow &&

{eyebrow}

} {title} {subtitle &&

{subtitle}

}
)}
    {items.map((item, i) => { const on = open === i; return (
  • {on && ( )}
    {on && (
    {item.answer}
    )}
  • ); })}
{answer && (

{String(shown + 1).padStart(2, "0")}

{answer.question}

{answer.answer}
)}
); }