Standard.site landing page built in Next.js
at main 76 lines 2.6 kB view raw
1'use client' 2 3import { useState } from 'react' 4import { motion, AnimatePresence } from 'motion/react' 5import { PlusIcon } from 'lucide-react' 6 7interface AccordionItemProps { 8 question: string 9 answer: string 10 isOpen: boolean 11 onToggle: () => void 12} 13 14function AccordionItem({ question, answer, isOpen, onToggle }: AccordionItemProps) { 15 return ( 16 <div className="rounded-xl border border-border bg-card overflow-hidden"> 17 <button 18 onClick={ onToggle } 19 className="flex w-full cursor-pointer items-center justify-between p-4 text-left" 20 > 21 <span className="font-medium text-base leading-snug tracking-tight text-base-content"> 22 { question } 23 </span> 24 <motion.div 25 animate={{ rotate: isOpen ? 135 : 0 }} 26 transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }} 27 > 28 <PlusIcon className="size-5 text-base-content" /> 29 </motion.div> 30 </button> 31 <AnimatePresence initial={ false }> 32 { isOpen && ( 33 <motion.div 34 initial={{ height: 0, opacity: 0 }} 35 animate={{ height: 'auto', opacity: 1 }} 36 exit={{ height: 0, opacity: 0 }} 37 transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }} 38 className="overflow-hidden" 39 > 40 <div className="border-t border-border p-4"> 41 <p className="text-base leading-snug tracking-tight text-muted"> 42 { answer } 43 </p> 44 </div> 45 </motion.div> 46 ) } 47 </AnimatePresence> 48 </div> 49 ) 50} 51 52interface AccordionProps { 53 items: { question: string; answer: string }[] 54} 55 56export function Accordion({ items }: AccordionProps) { 57 const [openIndex, setOpenIndex] = useState<number | null>(null) 58 59 const handleToggle = (index: number) => { 60 setOpenIndex(openIndex === index ? null : index) 61 } 62 63 return ( 64 <> 65 { items.map((item, index) => ( 66 <AccordionItem 67 key={ item.question } 68 question={ item.question } 69 answer={ item.answer } 70 isOpen={ openIndex === index } 71 onToggle={ () => handleToggle(index) } 72 /> 73 )) } 74 </> 75 ) 76}