Files
mr_decor/components/sections/other/apple-cards-carousel.tsx

201 lines
6.5 KiB
TypeScript

"use client";
import React, { useEffect, useRef, useState } from "react";
import {
motion,
useAnimationFrame,
useMotionTemplate,
useMotionValue,
useTransform,
animate,
} from "motion/react";
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
export function AppleCardsCarousel({
items,
direction = "left",
speed = "fast",
pauseOnHover = true,
className,
}: {
items: {
title: string;
description: string;
icon: React.ReactNode;
src?: string;
}[];
direction?: "left" | "right";
speed?: "fast" | "normal" | "slow";
pauseOnHover?: boolean;
className?: string;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const [start, setStart] = useState(false);
useEffect(() => {
setStart(true);
}, []);
const baseVelocity = {
fast: 50,
normal: 35,
slow: 20,
};
const baseX = useMotionValue(0);
const directionFactor = React.useRef<number>(1);
const [hovered, setHovered] = useState(false);
useAnimationFrame((t, delta) => {
if (hovered) return;
let moveBy = directionFactor.current * baseVelocity[speed] * (delta / 1000);
if (direction === "left") {
directionFactor.current = -1;
} else {
directionFactor.current = 1;
}
moveBy += directionFactor.current * moveBy * 0.5;
baseX.set(baseX.get() + moveBy);
});
const scrollLeft = () => {
if (scrollRef.current) {
// Calculate responsive card width + gap
const cardWidth = window.innerWidth < 640 ? 280 : window.innerWidth < 768 ? 320 : 350;
const scrollAmount = cardWidth + 16; // card width + gap
const newX = baseX.get() + scrollAmount;
animate(baseX, newX, { duration: 0.5, ease: "easeOut" });
}
};
const scrollRight = () => {
if (scrollRef.current) {
// Calculate responsive card width + gap
const cardWidth = window.innerWidth < 640 ? 280 : window.innerWidth < 768 ? 320 : 350;
const scrollAmount = cardWidth + 16; // card width + gap
const newX = baseX.get() - scrollAmount;
animate(baseX, newX, { duration: 0.5, ease: "easeOut" });
}
};
return (
<div className="space-y-6">
<div
ref={containerRef}
className={cn(
"relative w-full overflow-hidden [mask-image:linear-gradient(to_right,transparent_0%,white_10%,white_90%,transparent_100%)]",
className
)}
onMouseEnter={() => pauseOnHover && setHovered(true)}
onMouseLeave={() => pauseOnHover && setHovered(false)}
>
<div className="flex justify-center">
<motion.div
ref={scrollRef}
style={{
x: baseX,
willChange: "transform",
}}
className="flex flex-nowrap gap-4 items-center"
>
{start &&
items.map((item, idx) => (
<div
key={`item-${idx}`}
className="relative shrink-0 w-[280px] sm:w-[320px] md:w-[350px]"
>
<div className="group relative h-[400px] sm:h-[420px] md:h-[450px] w-full overflow-hidden rounded-3xl p-6 sm:p-7 md:p-8">
{item.src && (
<div className="absolute inset-0">
<img
src={item.src}
alt={item.title}
className="h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-br from-black/60 via-black/40 to-black/60" />
</div>
)}
<div className="absolute inset-0 bg-[linear-gradient(to_bottom_right,var(--background)_0%,rgba(255,255,255,0.08)_50%,transparent_60%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="relative z-10">
<div className="mb-4 flex items-center gap-4">
{item.icon}
<div>
<h3 className="text-heading-3 font-semibold text-overlay-foreground">
{item.title}
</h3>
</div>
</div>
<p className="text-body-sm text-overlay-foreground/90">
{item.description}
</p>
</div>
</div>
</div>
))}
{start &&
items.map((item, idx) => (
<div
key={`item-duplicate-${idx}`}
className="relative shrink-0 w-[280px] sm:w-[320px] md:w-[350px]"
>
<div className="group relative h-[400px] sm:h-[420px] md:h-[450px] w-full overflow-hidden rounded-3xl p-6 sm:p-7 md:p-8">
{item.src && (
<div className="absolute inset-0">
<img
src={item.src}
alt={item.title}
className="h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-br from-black/60 via-black/40 to-black/60" />
</div>
)}
<div className="absolute inset-0 bg-[linear-gradient(to_bottom_right,var(--background)_0%,rgba(255,255,255,0.08)_50%,transparent_60%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="relative z-10">
<div className="mb-4 flex items-center gap-4">
{item.icon}
<div>
<h3 className="text-heading-3 font-semibold text-overlay-foreground">
{item.title}
</h3>
</div>
</div>
<p className="text-body-sm text-overlay-foreground/90">
{item.description}
</p>
</div>
</div>
</div>
))}
</motion.div>
</div>
</div>
<div className="flex justify-center gap-4">
<Button
variant="outline"
size="icon-lg"
onClick={scrollRight}
aria-label="Scroll left"
>
<ChevronRight className="h-5 w-5" />
</Button>
<Button
variant="outline"
size="icon-lg"
onClick={scrollLeft}
aria-label="Scroll right"
>
<ChevronLeft className="h-5 w-5" />
</Button>
</div>
</div>
);
}