PreviewCodeGet Started EffortlesslyThree simple steps to bring your ideas to life1. Choose Your ComponentSelect the component that best suits your needs from Eldora UI's versatile collection, designed to simplify and enhance your development process.2. Add Utility HelpersEnhance functionality by incorporating utility helpers that align with the selected component, ensuring seamless integration and customization.3. Copy and Paste the CodeSimply copy and paste the provided code into your project, making the process quick and hassle-free. You're now ready to see the magic in action!1. Choose Your ComponentSelect the component that best suits your needs from Eldora UI's versatile collection, designed to simplify and enhance your development process.2. Add Utility HelpersEnhance functionality by incorporating utility helpers that align with the selected component, ensuring seamless integration and customization.3. Copy and Paste the CodeSimply copy and paste the provided code into your project, making the process quick and hassle-free. You're now ready to see the magic in action! Installation pnpmnpmyarnbunpnpm add framer-motion @radix-ui/react-accordion lucide-react CopyCopy and paste the following code of header into your project.components/eldoraui/features.tsx"use client"; import * as Accordion from "@radix-ui/react-accordion"; import { motion, useInView } from "framer-motion"; import type { ReactNode } from "react"; import React, { forwardRef, useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; type AccordionItemProps = { children: React.ReactNode; className?: string; } & Accordion.AccordionItemProps; const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>( ({ children, className, ...props }, forwardedRef) => ( <Accordion.Item className={cn( "mt-px overflow-hidden focus-within:relative focus-within:z-10", className, )} {...props} ref={forwardedRef} > {children} </Accordion.Item> ), ); AccordionItem.displayName = "AccordionItem"; interface AccordionTriggerProps { children: React.ReactNode; className?: string; } const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>( ({ children, className, ...props }, forwardedRef) => ( <Accordion.Header className="flex"> <Accordion.Trigger className={cn( "group flex flex-1 cursor-pointer items-center justify-between px-5 text-[15px] leading-none outline-none", className, )} {...props} ref={forwardedRef} > {children} </Accordion.Trigger> </Accordion.Header> ), ); AccordionTrigger.displayName = "AccordionTrigger"; type AccordionContentProps = { children: ReactNode; className?: string; } & Accordion.AccordionContentProps; const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>( ({ children, className, ...props }, forwardedRef) => ( <Accordion.Content className={cn( "data-[state=closed]:animate-slide-up data-[state=open]:animate-slide-down overflow-hidden text-[15px] font-medium", className, )} {...props} ref={forwardedRef} > <div className="px-5 py-2">{children}</div> </Accordion.Content> ), ); AccordionContent.displayName = "AccordionContent"; export interface FeaturesDataProps { id: number; title: string; content: string; image?: string; video?: string; icon?: React.ReactNode; } export interface FeaturesProps { collapseDelay?: number; ltr?: boolean; linePosition?: "left" | "right" | "top" | "bottom"; data: FeaturesDataProps[]; } export function Features({ collapseDelay = 5000, ltr = false, linePosition = "left", data = [], }: FeaturesProps) { const [currentIndex, setCurrentIndex] = useState<number>(-1); const carouselRef = useRef<HTMLUListElement>(null); const ref = useRef(null); const isInView = useInView(ref, { once: true, amount: 0.5, }); useEffect(() => { const timer = setTimeout(() => { if (isInView) { setCurrentIndex(0); } else { setCurrentIndex(-1); } }, 100); return () => clearTimeout(timer); }, [isInView]); const scrollToIndex = (index: number) => { if (carouselRef.current) { const card = carouselRef.current.querySelectorAll(".card")[index]; if (card) { const cardRect = card.getBoundingClientRect(); const carouselRect = carouselRef.current.getBoundingClientRect(); const offset = cardRect.left - carouselRect.left - (carouselRect.width - cardRect.width) / 2; carouselRef.current.scrollTo({ left: carouselRef.current.scrollLeft + offset, behavior: "smooth", }); } } }; useEffect(() => { const timer = setInterval(() => { setCurrentIndex((prevIndex) => prevIndex !== undefined ? (prevIndex + 1) % data.length : 0, ); }, collapseDelay); return () => clearInterval(timer); }, [collapseDelay, currentIndex, data.length]); useEffect(() => { const handleAutoScroll = () => { const nextIndex = (currentIndex !== undefined ? currentIndex + 1 : 0) % data.length; scrollToIndex(nextIndex); }; const autoScrollTimer = setInterval(handleAutoScroll, collapseDelay); return () => clearInterval(autoScrollTimer); }, [collapseDelay, currentIndex, data.length]); useEffect(() => { const carousel = carouselRef.current; if (carousel) { const handleScroll = () => { const scrollLeft = carousel.scrollLeft; const cardWidth = carousel.querySelector(".card")?.clientWidth || 0; const newIndex = Math.min( Math.floor(scrollLeft / cardWidth), data.length - 1, ); setCurrentIndex(newIndex); }; carousel.addEventListener("scroll", handleScroll); return () => carousel.removeEventListener("scroll", handleScroll); } }, [data.length]); return ( <section ref={ref} id="features"> <div className="container"> <div className="mx-auto max-w-6xl"> <div className="mx-auto my-12 grid h-full items-center gap-10 lg:grid-cols-2"> <div className={` order-1 hidden lg:order-none lg:flex ${ ltr ? "lg:order-2 lg:justify-end" : "justify-start" }`} > <Accordion.Root className="" type="single" defaultValue={`item-${currentIndex}`} value={`item-${currentIndex}`} onValueChange={(value) => setCurrentIndex(Number(value.split("-")[1])) } > {data.map((item, index) => ( <AccordionItem key={item.id} className="relative mb-8 last:mb-0" value={`item-${index}`} > {linePosition === "left" || linePosition === "right" ? ( <div className={`absolute inset-y-0 h-full w-0.5 overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${ linePosition === "right" ? "left-auto right-0" : "left-0 right-auto" }`} > <div className={`absolute left-0 top-0 w-full ${ currentIndex === index ? "h-full" : "h-0" } origin-top bg-primary transition-all ease-linear dark:bg-white`} style={{ transitionDuration: currentIndex === index ? `${collapseDelay}ms` : "0s", }} ></div> </div> ) : null} {linePosition === "top" || linePosition === "bottom" ? ( <div className={`absolute inset-x-0 h-0.5 w-full overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${ linePosition === "bottom" ? "bottom-0" : "top-0" }`} > <div className={`absolute left-0 ${ linePosition === "bottom" ? "bottom-0" : "top-0" } h-full ${ currentIndex === index ? "w-full" : "w-0" } origin-left bg-primary transition-all ease-linear dark:bg-white`} style={{ transitionDuration: currentIndex === index ? `${collapseDelay}ms` : "0s", }} ></div> </div> ) : null} <div className="relative flex items-center"> <div className="item-box mx-2 flex size-12 shrink-0 items-center justify-center rounded-full bg-primary/10 sm:mx-6"> {item.icon} </div> <div> <AccordionTrigger className="pl-0 text-xl font-bold"> {item.title} </AccordionTrigger> <AccordionTrigger className="justify-start pl-0 text-left text-[16px] leading-4"> {item.content} </AccordionTrigger> </div> </div> </AccordionItem> ))} </Accordion.Root> </div> <div className={`h-[350px] min-h-[200px] w-auto ${ ltr && "lg:order-1" }`} > {data[currentIndex]?.image ? ( <motion.img key={currentIndex} src={data[currentIndex].image} alt="feature" className="aspect-auto size-full rounded-xl border border-neutral-300/50 object-cover object-left-top p-1 shadow-lg" initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.98 }} transition={{ duration: 0.25, ease: "easeOut" }} /> ) : data[currentIndex]?.video ? ( <video preload="auto" src={data[currentIndex].video} className="aspect-auto size-full rounded-lg object-cover shadow-lg" autoPlay loop muted /> ) : ( <div className="aspect-auto size-full rounded-xl border border-neutral-300/50 bg-gray-200 p-1"></div> )} </div> <ul ref={carouselRef} className=" flex h-full snap-x snap-mandatory flex-nowrap overflow-x-auto py-10 [-ms-overflow-style:none] [-webkit-mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [scrollbar-width:none] lg:hidden [&::-webkit-scrollbar]:hidden" style={{ padding: "50px calc(50%)", }} > {data.map((item, index) => ( <div key={item.id} className="card relative mr-8 grid h-full max-w-60 shrink-0 items-start justify-center py-4 last:mr-0" onClick={() => setCurrentIndex(index)} style={{ scrollSnapAlign: "center", }} > <div className="absolute inset-y-0 left-0 right-auto h-0.5 w-full overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30"> <div className={`absolute left-0 top-0 h-full ${ currentIndex === index ? "w-full" : "w-0" } origin-top bg-primary transition-all ease-linear`} style={{ transitionDuration: currentIndex === index ? `${collapseDelay}ms` : "0s", }} ></div> </div> <h2 className="text-xl font-bold">{item.title}</h2> <p className="mx-0 max-w-sm text-balance text-sm"> {item.content} </p> </div> ))} </ul> </div> </div> </div> </section> ); }CopyExpand AccordionItem Props Prop NameTypeDefaultDescriptionchildrenReact.ReactNode-The content to be displayed inside the accordion item.classNamestring-Additional CSS classes to style the accordion item....propsAccordion.AccordionItemProps-Props inherited from Radix UI's AccordionItem. AccordionTrigger Props Prop NameTypeDefaultDescriptionchildrenReact.ReactNode-The trigger content (e.g., title or heading) for the accordion item.classNamestring-Additional CSS classes for styling the trigger....propsAccordion.AccordionTriggerProps-Props inherited from Radix UI's AccordionTrigger. AccordionContent Props Prop NameTypeDefaultDescriptionchildrenReact.ReactNode-The content to be displayed within the accordion.classNamestring-Additional CSS classes for styling the accordion content....propsAccordion.AccordionContentProps-Props inherited from Radix UI's AccordionContent. FeaturesDataProps (Used in Features) Prop NameTypeDefaultDescriptionidnumber-Unique identifier for each feature item.titlestring-The title of the feature.contentstring-The description or content of the feature.imagestring (optional)-URL of the image for the feature.videostring (optional)-URL of the video for the feature.iconReact.ReactNode (optional)-Icon or visual representation for the feature. Features Props Prop NameTypeDefaultDescriptioncollapseDelaynumber5000Time (in milliseconds) to auto-collapse an accordion item.ltrbooleanfalseDefines if the layout should switch to left-to-right.linePosition"left" | "right" | "top" | "bottom""left"Specifies the position of the progress line indicator.dataFeaturesDataProps[][]Array of feature objects containing title, content, image, video, etc.