Features 01
Features 01 é um componente de layout para React e Tailwind CSS, da biblioteca Eldora UI, com licença MIT. Copia e cola no teu projeto.
Eldora UI
MIT
layout
O que é
Features 01 é um componente de layout para React e Tailwind CSS, da biblioteca Eldora UI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion/react @radix-ui/react-accordion lucide-react
Licença
Este componente vem de Eldora UI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
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>
)
}