213 lines
7.9 KiB
TypeScript
213 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import { AnimatePresence, motion } from "framer-motion";
|
|
import { ArrowUpRight } from "lucide-react";
|
|
import React, { useEffect, useState } from "react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface Hero214Props {
|
|
className?: string;
|
|
}
|
|
|
|
const IMAGES = [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/lummi/landscape1.jpeg",
|
|
alt: "نمای معماری مدرن",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/lummi/landscape2.jpeg",
|
|
alt: "فضای معماری مینیمال",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/lummi/highRise.jpg",
|
|
alt: "ساختمان مرتفع معاصر",
|
|
},
|
|
] as const;
|
|
|
|
const Hero214 = ({ className }: Hero214Props) => {
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const secondaryA = IMAGES[(currentIndex + 1) % IMAGES.length];
|
|
const secondaryB = IMAGES[(currentIndex + 2) % IMAGES.length];
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
setCurrentIndex((prev) => (prev + 1) % IMAGES.length);
|
|
}, 2500);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [currentIndex]);
|
|
|
|
return (
|
|
<section
|
|
className={cn("relative w-full overflow-hidden bg-background py-20 md:py-28", className)}
|
|
>
|
|
<div className="container min-w-0">
|
|
{/*
|
|
Composition matches the reference:
|
|
title → top-left cutout, thumbs → top-right cutout, CTA → bottom-left notch.
|
|
Physical left/right align with the LTR SVG mask.
|
|
*/}
|
|
<div className="relative w-full min-w-0">
|
|
<h1 className="relative z-20 mb-6 max-w-[18rem] text-left font-sans text-3xl leading-[1.15] font-semibold tracking-tight text-foreground sm:max-w-xs sm:text-4xl md:mb-0 md:absolute md:top-0 md:left-0 md:max-w-[22rem] lg:max-w-[26rem] lg:text-5xl xl:text-[3.25rem]">
|
|
بلاکهای آماده با رابط کاربری و Tailwind
|
|
</h1>
|
|
|
|
<div className="relative z-10 mt-2 w-full md:mt-10 lg:mt-14">
|
|
<MaskedDiv maskType="type-5">
|
|
<AnimatePresence mode="wait" initial={false}>
|
|
<motion.img
|
|
key={currentIndex}
|
|
className="absolute inset-0 size-full object-cover"
|
|
src={IMAGES[currentIndex].src}
|
|
alt={IMAGES[currentIndex].alt}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 1.2 }}
|
|
/>
|
|
</AnimatePresence>
|
|
</MaskedDiv>
|
|
|
|
{/* Top-right secondary thumbs (sit in the mask cutout) */}
|
|
<div className="absolute top-0 right-0 z-20 hidden gap-4 md:flex lg:gap-5 xl:gap-6">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 24, scale: 0.94 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
transition={{ duration: 0.35, ease: "easeOut" }}
|
|
className="size-28 overflow-hidden rounded-[1.75rem] lg:size-36 xl:size-44 xl:rounded-[2rem]"
|
|
>
|
|
<AnimatePresence mode="wait" initial={false}>
|
|
<motion.img
|
|
key={secondaryA.src}
|
|
src={secondaryA.src}
|
|
alt={secondaryA.alt}
|
|
className="size-full object-cover"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 1.2 }}
|
|
/>
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -24, scale: 0.94 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
transition={{ duration: 0.35, ease: "easeOut" }}
|
|
className="size-28 overflow-hidden rounded-[1.75rem] lg:size-36 xl:size-44 xl:rounded-[2rem]"
|
|
>
|
|
<AnimatePresence mode="wait" initial={false}>
|
|
<motion.img
|
|
key={secondaryB.src}
|
|
src={secondaryB.src}
|
|
alt={secondaryB.alt}
|
|
className="size-full object-cover"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 1.2 }}
|
|
/>
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Mobile thumbs */}
|
|
<div className="mt-4 flex w-full gap-4 md:hidden">
|
|
<div className="relative h-36 w-full overflow-hidden rounded-[1.75rem]">
|
|
<img
|
|
src={secondaryA.src}
|
|
alt={secondaryA.alt}
|
|
className="size-full object-cover"
|
|
/>
|
|
</div>
|
|
<div className="relative h-36 w-full overflow-hidden rounded-[1.75rem]">
|
|
<img
|
|
src={secondaryB.src}
|
|
alt={secondaryB.alt}
|
|
className="size-full object-cover"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom-left CTA in the mask notch */}
|
|
<Button
|
|
variant="secondary"
|
|
className="group relative z-20 mt-5 h-auto rounded-full bg-muted px-5 py-2.5 text-sm font-medium tracking-tight text-foreground hover:bg-muted/80 md:absolute md:bottom-1 md:left-0 md:mt-0 lg:bottom-2 xl:bottom-3"
|
|
>
|
|
مشاهده کامپوننتها
|
|
<ArrowUpRight className="ms-1.5 size-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export { Hero214 };
|
|
|
|
interface SvgPath {
|
|
path: string;
|
|
height: number;
|
|
width: number;
|
|
}
|
|
|
|
type MaskType = "type-5";
|
|
|
|
interface MaskedDivProps {
|
|
children: React.ReactNode;
|
|
maskType?: MaskType;
|
|
className?: string;
|
|
backgroundColor?: string;
|
|
size?: number;
|
|
}
|
|
|
|
const svgPaths: Record<MaskType, SvgPath> = {
|
|
// Notched organic frame: title pocket (top-left), thumbs pocket (top-right), CTA notch (bottom-left)
|
|
"type-5": {
|
|
path: "M0.589399 112.279C0.589402 82.1213 25.037 57.6738 55.1946 57.6738H335.688C350.06 57.6738 361.712 46.0226 361.712 31.6502C361.712 14.2835 375.79 0.205078 393.157 0.205078H949.833C983.496 0.205078 1010.78 27.4941 1010.78 61.1568C1010.78 89.0156 1033.37 111.6 1061.23 111.6H1472.74C1502.9 111.6 1527.35 136.047 1527.35 166.205V629.438C1527.35 659.596 1502.9 684.044 1472.74 684.044H639.176C619.635 684.044 603.794 668.203 603.794 648.662C603.794 629.122 587.954 613.281 568.413 613.281H55.1945C25.0369 613.281 0.589358 588.833 0.58936 558.676L0.589399 112.279Z",
|
|
height: 684,
|
|
width: 1528,
|
|
},
|
|
};
|
|
|
|
function buildMaskDataUrl(mask: SvgPath): string {
|
|
return `data:image/svg+xml,%3Csvg width='${mask.width}' height='${mask.height}' viewBox='0 0 ${mask.width} ${mask.height}' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='${mask.path}' fill='%23D9D9D9'/%3E%3C/svg%3E`;
|
|
}
|
|
|
|
const MaskedDiv: React.FC<MaskedDivProps> = ({
|
|
children,
|
|
maskType = "type-5",
|
|
className = "",
|
|
backgroundColor = "transparent",
|
|
size = 1,
|
|
}) => {
|
|
const selectedMask = svgPaths[maskType];
|
|
const maskUrl = buildMaskDataUrl(selectedMask);
|
|
|
|
const containerStyle: React.CSSProperties = {
|
|
aspectRatio: `${selectedMask.width} / ${selectedMask.height}`,
|
|
backgroundColor,
|
|
maskImage: `url("${maskUrl}")`,
|
|
WebkitMaskImage: `url("${maskUrl}")`,
|
|
maskRepeat: "no-repeat",
|
|
WebkitMaskRepeat: "no-repeat",
|
|
maskSize: "100% 100%",
|
|
WebkitMaskSize: "100% 100%",
|
|
maskPosition: "center",
|
|
WebkitMaskPosition: "center",
|
|
width: `${size * 100}%`,
|
|
maxWidth: "100%",
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={cn("relative w-full min-w-0 overflow-hidden", className)}
|
|
style={containerStyle}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|