Installation
pnpm add framer-motion lucide-react
pnpm add framer-motion lucide-react
Copy and paste the following code into your project.
components/eldoraui/integrations.tsx
"use client";
import { motion, useAnimation, useInView } from "framer-motion";
import {
BarChart,
File,
Globe,
HeartHandshake,
Rss,
Shield,
} from "lucide-react";
import { useEffect, useId, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { Marquee } from "@/components/eldoraui/marquee";
const tiles = [
{
icon: <HeartHandshake className="size-full" />,
bg: (
<div className="pointer-events-none absolute left-1/2 top-1/2 size-1/2 -translate-x-1/2 -translate-y-1/2 overflow-visible rounded-full bg-gradient-to-r from-orange-600 via-rose-600 to-violet-600 opacity-70 blur-[20px]"></div>
),
},
{
icon: <Globe className="size-full" />,
bg: (
<div className="pointer-events-none absolute left-1/2 top-1/2 size-1/2 -translate-x-1/2 -translate-y-1/2 overflow-visible rounded-full bg-gradient-to-r from-cyan-500 via-blue-500 to-indigo-500 opacity-70 blur-[20px]"></div>
),
},
{
icon: <File className="size-full" />,
bg: (
<div className="pointer-events-none absolute left-1/2 top-1/2 size-1/2 -translate-x-1/2 -translate-y-1/2 overflow-visible rounded-full bg-gradient-to-r from-green-500 via-teal-500 to-emerald-600 opacity-70 blur-[20px]"></div>
),
},
{
icon: <Shield className="size-full" />,
bg: (
<div className="pointer-events-none absolute left-1/2 top-1/2 size-1/2 -translate-x-1/2 -translate-y-1/2 overflow-visible rounded-full bg-gradient-to-r from-yellow-400 via-orange-500 to-yellow-600 opacity-70 blur-[20px]"></div>
),
},
{
icon: <Rss className="size-full" />,
bg: (
<div className="pointer-events-none absolute left-1/2 top-1/2 size-1/2 -translate-x-1/2 -translate-y-1/2 overflow-visible rounded-full bg-gradient-to-r from-orange-600 via-rose-600 to-violet-600 opacity-70 blur-[20px]"></div>
),
},
{
icon: <BarChart className="size-full" />,
bg: (
<div className="pointer-events-none absolute left-1/2 top-1/2 size-1/2 -translate-x-1/2 -translate-y-1/2 overflow-visible rounded-full bg-gradient-to-r from-gray-600 via-gray-500 to-gray-400 opacity-70 blur-[20px]"></div>
),
},
];
function shuffleArray(array: any[]) {
let currentIndex = array.length;
let randomIndex;
// While there remain elements to shuffle.
while (currentIndex !== 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex],
array[currentIndex],
];
}
return array;
}
function Card(card: { icon: JSX.Element; bg: JSX.Element }) {
const id = useId();
const controls = useAnimation();
const ref = useRef(null);
const inView = useInView(ref, { once: true });
useEffect(() => {
if (inView) {
controls.start({
opacity: 1,
transition: { delay: Math.random() * 2, ease: "easeOut", duration: 1 },
});
}
}, [controls, inView]);
return (
<motion.div
key={id}
ref={ref}
initial={{ opacity: 0 }}
animate={controls}
className={cn(
"relative size-20 cursor-pointer overflow-hidden rounded-2xl border p-4",
// light styles
"bg-white [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]",
// dark styles
"transform-gpu dark:bg-transparent dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]",
)}
>
{card.icon}
{card.bg}
</motion.div>
);
}
export function Integrations() {
const [randomTiles1, setRandomTiles1] = useState<typeof tiles>([]);
const [randomTiles2, setRandomTiles2] = useState<typeof tiles>([]);
const [randomTiles3, setRandomTiles3] = useState<typeof tiles>([]);
const [randomTiles4, setRandomTiles4] = useState<typeof tiles>([]);
useEffect(() => {
if (typeof window !== "undefined") {
// Ensures this runs client-side
setRandomTiles1(shuffleArray([...tiles]));
setRandomTiles2(shuffleArray([...tiles]));
setRandomTiles3(shuffleArray([...tiles]));
setRandomTiles4(shuffleArray([...tiles]));
}
}, []);
return (
<section id="cta">
<div className="container mx-auto px-4 py-12 md:px-8">
<div className="flex w-full flex-col items-center justify-center">
<div className="relative flex w-full flex-col items-center justify-center overflow-hidden">
<Marquee
reverse
className="-delay-[200ms] [--duration:10s]"
repeat={5}
>
{randomTiles1.map((review, idx) => (
<Card key={idx} {...review} />
))}
</Marquee>
<Marquee reverse className="[--duration:25s]" repeat={5}>
{randomTiles2.map((review, idx) => (
<Card key={idx} {...review} />
))}
</Marquee>
<Marquee
reverse
className="-delay-[200ms] [--duration:20s]"
repeat={5}
>
{randomTiles1.map((review, idx) => (
<Card key={idx} {...review} />
))}
</Marquee>
<Marquee reverse className="[--duration:30s]" repeat={5}>
{randomTiles2.map((review, idx) => (
<Card key={idx} {...review} />
))}
</Marquee>
<Marquee
reverse
className="-delay-[200ms] [--duration:20s]"
repeat={5}
>
{randomTiles3.map((review, idx) => (
<Card key={idx} {...review} />
))}
</Marquee>
<Marquee reverse className="[--duration:30s]" repeat={5}>
{randomTiles4.map((review, idx) => (
<Card key={idx} {...review} />
))}
</Marquee>
<div className="absolute ">
<div className="bg-backtround dark:bg-background absolute inset-0 -z-10 rounded-full opacity-40 blur-xl" />
</div>
<div className="to-backtround dark:to-background absolute inset-x-0 bottom-0 h-full bg-gradient-to-b from-transparent to-70%" />
</div>
</div>
</div>
</section>
);
}
Copy and paste the following code into your project.
components/eldoraui/marquee.tsx
import { cn } from "@/lib/utils";
interface MarqueeProps {
className?: string;
reverse?: boolean;
pauseOnHover?: boolean;
children?: React.ReactNode;
vertical?: boolean;
repeat?: number;
[key: string]: any;
}
export function Marquee({
className,
reverse,
pauseOnHover = false,
children,
vertical = false,
repeat = 4,
...props
}: MarqueeProps) {
return (
<div
{...props}
className={cn(
"group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]",
{
"flex-row": !vertical,
"flex-col": vertical,
},
className,
)}
>
{Array(repeat)
.fill(0)
.map((_, i) => (
<div
key={i}
className={cn("flex shrink-0 justify-around [gap:var(--gap)]", {
"animate-marquee flex-row": !vertical,
"animate-marquee-vertical flex-col": vertical,
"group-hover:[animation-play-state:paused]": pauseOnHover,
"[animation-direction:reverse]": reverse,
})}
>
{children}
</div>
))}
</div>
);
}
Update the import paths to match your project setup.
Update tailwind.config.js
Add the following animations to your tailwind.config.js
file:
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
animation: {
marquee: "marquee var(--duration) linear infinite",
"marquee-vertical": "marquee-vertical var(--duration) linear infinite",
},
keyframes: {
marquee: {
from: { transform: "translateX(0)" },
to: { transform: "translateX(calc(-100% - var(--gap)))" },
},
"marquee-vertical": {
from: { transform: "translateY(0)" },
to: { transform: "translateY(calc(-100% - var(--gap)))" },
},
},
},
},
};
Props
Integrations
Component Props
Prop Name | Type | Default | Description |
---|---|---|---|
icon | JSX.Element | - | The icon to be displayed inside the card (e.g., from lucide-react ). |
bg | JSX.Element | - | The background element to be displayed behind the icon (a styled div). |
Internal Variables
Variable Name | Type | Description |
---|---|---|
randomTiles1 | typeof tiles | A shuffled array of tile data for the first marquee animation. |
randomTiles2 | typeof tiles | A shuffled array of tile data for the second marquee animation. |
randomTiles3 | typeof tiles | A shuffled array of tile data for the third marquee animation. |
randomTiles4 | typeof tiles | A shuffled array of tile data for the fourth marquee animation. |
tiles
Array
Property Name | Type | Description |
---|---|---|
icon | JSX.Element | The icon to be displayed inside the card (e.g., from lucide-react ). |
bg | JSX.Element | The background element to be displayed behind the icon (a styled div). |