All components

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.

Open in the catalogue

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

registry/blocks/features-01/page.tsx
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>
  )
}
registry/blocks/features-01/components/features.tsx
"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>
  )
}
registry/blocks/features-01/components/section.tsx
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>
  )
}

Similar components