Components
Loading preview...
Animated hero section with carousel.
npx shadcn@latest add https://21st.dev/r/cnippet.dev/flow-hero-1"use client";
import Autoplay from "embla-carousel-autoplay";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { ArrowRight } from "lucide-react";
import Image from "next/image";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/demos/ui/carousel";
import { cn } from "@/lib/utils";
const ITEMS_COUNT = 5;
export default function Hero() {
const [mainApi, setMainApi] = useState<CarouselApi>();
const [thumbApi, setThumbApi] = useState<CarouselApi>();
const [selectedIndex, setSelectedIndex] = useState(0);
const onThumbClick = useCallback(
(index: number) => {
if (!mainApi || !thumbApi) return;
mainApi.scrollTo(index);
},
[mainApi, thumbApi],
);
const onSelect = useCallback(() => {
if (!mainApi || !thumbApi) return;
const index = mainApi.selectedScrollSnap();
setSelectedIndex(index);
thumbApi.scrollTo(index);
}, [mainApi, thumbApi]);
const refs = useRef<any[]>([]);
const container = useRef(null);
const createAnimation = () => {
gsap.to(refs.current, {
ease: "none",
opacity: 1,
scrollTrigger: {
end: `+=${window.innerHeight / 1.5}`,
scrub: true,
start: "top",
trigger: container.current,
},
stagger: 0.1,
});
};
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
createAnimation();
}, [createAnimation]);
useEffect(() => {
if (!mainApi) return;
onSelect();
mainApi.on("select", onSelect);
mainApi.on("reInit", onSelect);
return () => {
mainApi.off("select", onSelect);
mainApi.off("reInit", onSelect);
};
}, [mainApi, onSelect]);
return (
<div className="mx-auto w-full max-w-7xl p-4" ref={container}>
<div className="relative w-full overflow-hidden rounded-xl">
<Carousel
className="w-full"
plugins={[
Autoplay({
delay: 4000,
}),
]}
setApi={setMainApi}
>
<CarouselContent>
{Array.from({ length: ITEMS_COUNT }).map((_, index) => (
<CarouselItem key={index}>
<div className="relative aspect-video w-full overflow-hidden bg-muted">
<Image
alt={`Slide ${index + 1}`}
className="object-cover"
fill
priority={index === 0}
src={`https://images.cnippet.dev/image/upload/v1770400411/h${index+2}.jpg`}
/>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="absolute inset-x-0 bottom-0 bg-linear-to-t from-black/70 to-transparent p-4 transition-opacity duration-300">
<div className="flex items-end justify-between px-10">
<div className="relative w-full max-w-md">
<Carousel
className="w-full"
opts={{
containScroll: "keepSnaps",
dragFree: true,
}}
setApi={setThumbApi}
>
<CarouselContent className="-ml-2 flex-row items-end">
{Array.from({ length: ITEMS_COUNT }).map((_, index) => (
<CarouselItem
className="basis-1/4 cursor-pointer pl-2 sm:basis-1/7"
key={index}
onClick={() => onThumbClick(index)}
>
<div
className={cn(
"relative overflow-hidden transition-all duration-300",
index === selectedIndex
? "h-20 opacity-100"
: "aspect-square border-white/40 opacity-50 hover:opacity-80",
)}
>
<Image
alt={`Thumb ${index + 2}`}
className="object-cover h-full"
fill
src={`https://images.cnippet.dev/image/upload/v1770400411/h${index+2}.jpg`}
/>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
<div className="flex max-w-sm flex-col items-end gap-6">
<div className="flex items-center gap-3">
<div className="flex gap-1">
{[...Array(4)].map((_, i) => (
<span className="text-2xl text-orange-500" key={i}>
ā
</span>
))}
<span className="text-2xl text-gray-400">ā
</span>
</div>
<span className="font-semibold text-lg text-white">
4 / 5
</span>
</div>
<p className="text-right text-lg text-white leading-relaxed">
We crafts modern, clean, and purposeful digital experiences that
elevate brands with simplicity, clarity, and strong visual
design.
</p>
<Button
className="group relative overflow-hidden px-2 font-normal text-lg text-white hover:bg-transparent"
variant="ghost"
>
View Services <ArrowRight />
<div className="absolute bottom-0 z-20 h-px w-full bg-gray-500" />
<div className="absolute bottom-0 z-30 h-px w-full -translate-x-40 bg-white transition-all duration-700 ease-in-out group-hover:translate-x-0" />
</Button>
</div>
</div>
<div className="">
<h1 className="text-center font-semibold text-8xl text-white tracking-wide">
FLOW + STUDIO
</h1>
</div>
</div>
</div>
</div>
);
}