Features 01
Features 01 is a React and Tailwind CSS layout component from Eldora UI, licensed MIT. Copy it and paste it into your project.
Eldora UI
MIT
layout
What it is
Features 01 is a React and Tailwind CSS layout component from Eldora UI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion/react @radix-ui/react-accordion lucide-react
Licence
This component comes from Eldora UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Features 01
import { Eye, PackageSearch, Settings } from "lucide-react"
import { Features } from "@/registry/blocks/features-01/components/features"
import Section from "@/registry/blocks/features-01/components/section"
const data = [
{
id: 1,
title: "1. Choose Your Component",
content:
"Select the component that best suits your needs from Eldora UI's versatile collection, designed to simplify and enhance your development process.",
image:
"https://pub-b3533f2e1c954842824758490b95c9d5.r2.dev/block-preview-dark.png",
icon: <PackageSearch className="text-primary size-6" />,
},
{
id: 2,
title: "2. Add Utility Helpers",
content:
"Enhance functionality by incorporating utility helpers that align with the selected component, ensuring seamless integration and customization.",
image:
"https://pub-b3533f2e1c954842824758490b95c9d5.r2.dev/block-preview-dark.png",
icon: <Settings className="text-primary size-6" />,
},
{
id: 3,
title: "3. Copy and Paste the Code",
content:
"Simply 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!",
image:
"https://pub-b3533f2e1c954842824758490b95c9d5.r2.dev/block-preview-dark.png",
icon: <Eye className="text-primary size-6" />,
},
]
export default function Testimonals02Page() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-full">
<Section
title="Get Started Effortlessly"
subtitle="Three simple steps to bring your ideas to life"
>
<Features data={data} />
</Section>
</div>
</div>
)
}
"use client"
import type { ReactNode } from "react"
import React, { forwardRef, useEffect, useRef, useState } from "react"
import * as Accordion from "@radix-ui/react-accordion"
import { motion, useInView } from "motion/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"
? "right-0 left-auto"
: "right-auto left-0"
}`}
>
<div
className={`absolute top-0 left-0 w-full ${
currentIndex === index ? "h-full" : "h-0"
} bg-primary origin-top 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"
} bg-primary origin-left 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 bg-primary/10 mx-2 flex size-12 shrink-0 items-center justify-center rounded-full 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 [mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] py-10 [-ms-overflow-style:none] [-webkit-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 right-auto left-0 h-0.5 w-full overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30">
<div
className={`absolute top-0 left-0 h-full ${
currentIndex === index ? "w-full" : "w-0"
} bg-primary origin-top 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-sm text-balance">
{item.content}
</p>
</div>
))}
</ul>
</div>
</div>
</div>
</section>
)
}
interface SectionProps {
id?: string
title?: string
subtitle?: string
description?: string
children?: React.ReactNode
className?: string
}
export default function Section({
id,
title,
subtitle,
description,
children,
className,
}: SectionProps) {
const sectionId = title ? title.toLowerCase().replace(/\s+/g, "-") : id
return (
<section id={id || sectionId}>
<div className={className}>
<div className="relative container mx-auto max-w-7xl px-4 py-16">
<div className="mx-auto space-y-4 pb-6 text-center">
{title && (
<h2 className="text-primary font-mono text-sm font-medium tracking-wider uppercase">
{title}
</h2>
)}
{subtitle && (
<h3 className="mx-auto mt-4 max-w-xs text-3xl font-semibold sm:max-w-none sm:text-4xl md:text-5xl">
{subtitle}
</h3>
)}
{description && (
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-slate-600">
{description}
</p>
)}
</div>
{children}
</div>
</div>
</section>
)
}