"use client";
import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { ArrowUpRight, MessageCircle, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
export type FaqItem = { question: string; answer: React.ReactNode };
export interface FaqAccordionProps {
eyebrow?: string;
title?: string;
description?: string;
items?: FaqItem[];
/** Only one item open at a time (default true). */
single?: boolean;
/** Indexes open on first render. */
defaultOpen?: number[];
contact?: { label: string; href: string; note?: string } | null;
className?: string;
}
const DEFAULT_ITEMS: FaqItem[] = [
{
question: "How does the 14-day free trial work?",
answer: "You get full access to every Pro feature for 14 days — no credit card required. When the trial ends you can pick a plan or drop back to the free tier. Your data stays exactly where you left it.",
},
{
question: "Can I change plans later?",
answer: "Yes. Upgrades take effect immediately and are prorated to the day. Downgrades apply at the end of your current billing period, so you never lose features you've already paid for.",
},
{
question: "Where is my data stored?",
answer: "Customer data is stored in encrypted databases in the EU or US — you choose the region when creating a workspace. Backups are taken every hour and retained for 30 days.",
},
{
question: "Do you offer discounts for startups and non-profits?",
answer: "We do. Early-stage startups get 50% off the first year and registered non-profits get 30% off forever. Reach out to our team with a little info about your organization.",
},
{
question: "Is there an API?",
answer: "Every action in the app is available through our REST and GraphQL APIs, with typed SDKs for TypeScript, Python and Go. Rate limits are generous and listed on each plan.",
},
{
question: "What happens if I cancel?",
answer: "You can cancel anytime from billing settings in two clicks. You'll keep access until the end of the period and can export everything as CSV or JSON before you go.",
},
];
export function FaqAccordion({
eyebrow = "FAQ",
title = "Questions, answered",
description = "Everything you need to know about the product and billing. Can't find what you're looking for? Our team replies within a few hours.",
items = DEFAULT_ITEMS,
single = true,
defaultOpen = [0],
contact = { label: "Chat with our team", href: "#contact", note: "Average reply time: 2 hours" },
className,
}: FaqAccordionProps) {
const [open, setOpen] = React.useState<Set<number>>(() => new Set(single ? defaultOpen.slice(0, 1) : defaultOpen));
const baseId = React.useId();
const buttons = React.useRef<(HTMLButtonElement | null)[]>([]);
const reduce = useReducedMotion();
const toggle = (i: number) =>
setOpen((prev) => {
const next = new Set(single ? [] : prev);
if (!prev.has(i)) next.add(i);
return next;
});
const onKeyDown = (e: React.KeyboardEvent, i: number) => {
const n = items.length;
const focus = (j: number) => buttons.current[(j + n) % n]?.focus();
const keys: Record<string, number> = { ArrowDown: i + 1, ArrowUp: i - 1, Home: 0, End: n - 1 };
const target = keys[e.key];
if (target === undefined) return;
e.preventDefault();
focus(target);
};
return (
<section className={cn("relative w-full bg-background px-4 py-16 sm:px-6 sm:py-24", className)}>
<div className="mx-auto grid max-w-6xl gap-12 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] lg:gap-20">
<div className="lg:sticky lg:top-12 lg:self-start">
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-primary">{eyebrow}</p>
<h2 className="mt-3 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">{title}</h2>
<p className="mt-5 max-w-md text-pretty text-base leading-relaxed text-muted-foreground">{description}</p>
{contact && (
<a
href={contact.href}
className="group mt-8 flex max-w-sm items-center gap-4 rounded-2xl border bg-card p-4 transition hover:border-primary/40 hover:shadow-lg hover:shadow-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="grid size-11 shrink-0 place-items-center rounded-xl bg-gradient-to-br from-violet-500 to-fuchsia-500 text-white shadow-md">
<MessageCircle aria-hidden className="size-5" />
</span>
<span className="min-w-0 flex-1">
<span className="block font-semibold text-foreground">{contact.label}</span>
{contact.note && <span className="block text-sm text-muted-foreground">{contact.note}</span>}
</span>
<ArrowUpRight aria-hidden className="size-5 text-muted-foreground transition group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-foreground" />
</a>
)}
</div>
<div className="divide-y border-y">
{items.map((item, i) => {
const isOpen = open.has(i);
const btnId = `${baseId}-q${i}`;
const panelId = `${baseId}-a${i}`;
return (
<div key={i} className="relative">
<h3>
<button
ref={(el) => {
buttons.current[i] = el;
}}
id={btnId}
type="button"
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => toggle(i)}
onKeyDown={(e) => onKeyDown(e, i)}
className="group flex w-full items-center justify-between gap-6 rounded-lg py-6 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background"
>
<span className={cn("text-base font-medium transition-colors sm:text-lg", isOpen ? "text-foreground" : "text-foreground/80 group-hover:text-foreground")}>
{item.question}
</span>
<motion.span
aria-hidden
animate={{ rotate: isOpen ? 135 : 0 }}
transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 300, damping: 20 }}
className={cn(
"grid size-8 shrink-0 place-items-center rounded-full border transition-colors",
isOpen ? "border-transparent bg-primary text-primary-foreground" : "bg-card text-muted-foreground group-hover:text-foreground",
)}
>
<Plus className="size-4" strokeWidth={2.25} />
</motion.span>
</button>
</h3>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
id={panelId}
role="region"
aria-labelledby={btnId}
key="panel"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={reduce ? { duration: 0 } : { height: { duration: 0.35, ease: [0.22, 1, 0.36, 1] }, opacity: { duration: 0.25 } }}
className="overflow-hidden"
>
<motion.div
initial={{ y: reduce ? 0 : -6 }}
animate={{ y: 0 }}
exit={{ y: reduce ? 0 : -6 }}
className="pb-6 pr-2 text-pretty sm:pr-12 leading-relaxed text-muted-foreground"
>
{item.answer}
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
</div>
</section>
);
}