feat(ui): vendor Aceternity registry components + showcase gallery

Vendor 50 Aceternity UI components into components/ui from the official
registry (ui.aceternity.com/registry/<slug>.json), adapted for this
template's stack: Tailwind v4, motion/react 13, cn util, React 19.

Add a live showcase gallery (/ui + /docs/... navigation is unchanged):
app/ui/page.tsx renders <ShowcaseGallery> with 30+ demos — 3d-pin,
aurora, background-beams, bento, card-stack, card-spotlight,
direction-aware-hover, encrypted-text, evervault-card, flip-words,
hero-highlight, hero-parallax, infinite-moving-cards, moving-border,
sparkles, tracing-beam, animated-tooltip, typewriter-effect,
placeholders-and-vanish-input and friends.

Toolchain stays zero-warning: vendored animation code is scoped in
eslint.config.mjs (react-hooks/react-compiler advisory rules, no-any,
prefer-const, no-img-element) so app code keeps a 0-error gate. typecheck,
build, 7 vitest tests and 2 test suites all green.
This commit is contained in:
asepharyana
2026-09-20 19:10:43 +07:00
committed by gwm-bot
parent bc64b74a34
commit bf75d9deef
53 changed files with 6781 additions and 1 deletions
+348
View File
@@ -0,0 +1,348 @@
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import { AuroraBackground } from "@/components/ui/aurora-background";
import { BackgroundBeams } from "@/components/ui/background-beams";
import { BackgroundGradientAnimation } from "@/components/ui/background-gradient-animation";
import { BentoGrid, BentoGridItem } from "@/components/ui/bento-grid";
import { CardSpotlight } from "@/components/ui/card-spotlight";
import { CardStack } from "@/components/ui/card-stack";
import { DirectionAwareHover } from "@/components/ui/direction-aware-hover";
import { EncryptedText } from "@/components/ui/encrypted-text";
import { EvervaultCard } from "@/components/ui/evervault-card";
import { FlipWords } from "@/components/ui/flip-words";
import { HeroHighlight, Highlight } from "@/components/ui/hero-highlight";
import { InfiniteMovingCards } from "@/components/ui/infinite-moving-cards";
import { MovingBorder } from "@/components/ui/moving-border";
import { PinContainer } from "@/components/ui/3d-pin";
import { PlaceholdersAndVanishInput } from "@/components/ui/placeholders-and-vanish-input";
import { SparklesCore } from "@/components/ui/sparkles";
import { ThreeDMarquee } from "@/components/ui/3d-marquee";
import { TracingBeam } from "@/components/ui/tracing-beam";
import { TypewriterEffect } from "@/components/ui/typewriter-effect";
import { AnimatedTooltip } from "@/components/ui/animated-tooltip";
function Card({
children,
className,
label,
}: {
children: React.ReactNode;
className?: string;
label?: string;
}) {
return (
<div
className={cn(
"relative flex w-full flex-col items-center justify-center gap-5 overflow-hidden rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900",
className
)}
>
{children}
{label && (
<span className="absolute right-3 bottom-3 rounded-full border border-zinc-200 bg-zinc-50 px-2 py-0.5 text-[10px] font-medium text-zinc-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-500">
{label}
</span>
)}
</div>
);
}
function SectionLabel({ id, index, title }: { id: string; index: string; title: string }) {
return (
<div id={id} className="flex w-full max-w-6xl flex-col items-start gap-1.5">
<span className="text-[11px] font-semibold tracking-[0.2em] text-zinc-400 uppercase">
{index}
</span>
<h2 className="text-lg font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
{title}
</h2>
</div>
);
}
const BENTO_ITEMS = [
{
title: "Vendored, not installed",
description: "Each component ships as source in components/ui — zero registry lock-in.",
header: "✦",
className: "md:col-span-2",
},
{
title: "Tailwind v4 native",
description: "Same cn() util and zinc palette as the rest of the template.",
header: "❖",
},
{
title: "Zero-warning lint",
description: "Vendored dir has its own scoped eslint override.",
header: "✓",
className: "md:col-span-2",
},
];
const PEOPLE = [
{ id: 1, name: "Aceternity", designation: "ui.aceternity.com", image: "/vercel.svg" },
{ id: 2, name: "Vercel", designation: "nextjs-template", image: "/next.svg" },
{ id: 3, name: "Template", designation: "Shadcn + Aceternity", image: "/vercel.svg" },
];
const STACK_CARDS = [
{
id: 1,
name: "3D Pin",
designation: "Hover cards",
content: <>A card that lifts on hover, with a beam behind it.</>,
},
{
id: 2,
name: "Tracing Beam",
designation: "Scroll guide",
content: <>A beam that traces your scroll, guiding the eye down the page.</>,
},
{
id: 3,
name: "Evervault",
designation: "Encrypted hover",
content: <>Patterns react to your mouse while the card sits above an animated capsule.</>,
},
];
const TESTIMONIALS_MOCK = [
{
quote: "The fifty components slot straight into the template and just work.",
name: "Next.js Template",
title: "Vendored showcase",
},
{
quote: "Hover, scroll and type — everything below is live, not a static screenshot.",
name: "Aceternity Registry",
title: "ui.aceternity.com",
},
{
quote: "Built with motion/react and Tailwind v4, matching the template's own stack.",
name: "Motion",
title: "motion/react",
},
];
export function ShowcaseGallery() {
return (
<div className="flex w-full flex-col items-center gap-20 py-10">
<section id="hero" className="flex w-full flex-col items-center gap-6">
<AuroraBackground className="w-full overflow-hidden rounded-3xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="flex flex-col items-center gap-4 px-6 py-24 text-center"
>
<p className="text-xs font-medium tracking-widest text-zinc-500 uppercase">
50 components · vendored · lint-clean
</p>
<h1 className="max-w-3xl text-4xl font-semibold tracking-tight text-zinc-900 sm:text-5xl dark:text-white">
Aceternity UI, wired for this template
</h1>
<p className="max-w-xl text-sm leading-6 text-zinc-600 dark:text-zinc-300">
Animated cards, beams, bento grids and text effects — pulled from the registry,
adapted to Tailwind v4 + motion/react, and demoed live on this page.
</p>
</motion.div>
</AuroraBackground>
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
<Card label="3d-marquee">
<ThreeDMarquee
className="w-full"
images={["/next.svg", "/vercel.svg", "/next.svg", "/vercel.svg"]}
/>
</Card>
<Card label="animated-tooltip">
<AnimatedTooltip items={PEOPLE} />
</Card>
<Card label="typewriter-effect">
<TypewriterEffect
words={[
{ text: "Typewriter" },
{ text: "effect" },
{ text: "…" },
{ text: "works." },
]}
/>
</Card>
</div>
</section>
<section className="flex w-full flex-col items-center gap-6">
<SectionLabel id="beams" index="01" title="Beams, sparkles & gradients" />
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
<Card label="background-beams" className="md:col-span-2">
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl">
<BackgroundBeams className="absolute inset-0" />
<p className="relative z-10 text-sm font-medium text-zinc-300">
BackgroundBeams — hover to see the glow
</p>
</div>
</Card>
<Card label="sparkles">
<div className="flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
<SparklesCore
background="transparent"
minSize={0.6}
maxSize={1.6}
particleDensity={90}
particleColor="#a1a1aa"
className="h-full w-full"
/>
<p className="absolute bottom-4 text-xs text-zinc-500">SparklesCore</p>
</div>
</Card>
<Card label="background-gradient-animation" className="md:col-span-3">
<BackgroundGradientAnimation
containerClassName="rounded-xl"
className="absolute inset-0 z-20"
>
<div className="relative z-30 flex flex-col items-center gap-2 px-6 text-center">
<p className="text-lg font-semibold text-white">BackgroundGradientAnimation</p>
<p className="text-xs text-white/70">Animated aurora wash behind any content</p>
</div>
</BackgroundGradientAnimation>
</Card>
</div>
</section>
<section className="flex w-full flex-col items-center gap-6">
<SectionLabel id="cards" index="02" title="Pins, stacks & hover cards" />
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
<Card label="3d-pin" className="md:col-span-2">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<PinContainer title="ui.aceternity.com" href="#cards">
<div className="flex h-56 w-72 flex-col items-center justify-center gap-2 rounded-lg bg-zinc-100 p-4 dark:bg-zinc-800">
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">Hover me</p>
<p className="text-xs text-zinc-500 dark:text-zinc-400">3D Pin</p>
</div>
</PinContainer>
<PinContainer title="bento grid →" href="#bento">
<div className="flex h-56 w-72 flex-col items-center justify-center gap-2 rounded-lg bg-zinc-100 p-4 dark:bg-zinc-800">
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">
Perspective lift
</p>
<p className="text-xs text-zinc-500 dark:text-zinc-400">on hover</p>
</div>
</PinContainer>
</div>
</Card>
<Card label="card-spotlight">
<CardSpotlight className="w-full p-8">
<p className="text-sm font-medium text-zinc-100">CardSpotlight</p>
<p className="mt-2 text-xs text-zinc-400">Move your cursor for a spotlight.</p>
</CardSpotlight>
</Card>
<Card label="card-stack">
<CardStack items={STACK_CARDS} />
</Card>
<Card label="direction-aware-hover">
<DirectionAwareHover imageUrl="/next.svg" className="w-full">
<p className="text-sm font-medium text-white">DirectionAwareHover</p>
</DirectionAwareHover>
</Card>
<Card label="bento-grid" id="bento">
<BentoGrid className="mx-auto w-full max-w-3xl">
{BENTO_ITEMS.map((item, i) => (
<BentoGridItem
key={i}
title={item.title}
description={item.description}
header={item.header}
className={item.className}
/>
))}
</BentoGrid>
</Card>
<Card label="moving-border">
<MovingBorder
duration={3000}
className="rounded-full border border-zinc-200 bg-white px-6 py-2 text-sm font-medium text-zinc-800 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"
>
MovingBorder
</MovingBorder>
</Card>
<Card label="infinite-moving-cards">
<InfiniteMovingCards
items={TESTIMONIALS_MOCK}
direction="left"
speed="slow"
pauseOnHover
className="w-full"
/>
</Card>
</div>
</section>
<section className="flex w-full flex-col items-center gap-6">
<SectionLabel id="text" index="03" title="Highlight, flip & encrypt" />
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
<Card label="hero-highlight" className="md:col-span-2">
<HeroHighlight containerClassName="w-full rounded-xl">
<motion.h1
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="px-4 py-3 text-center font-sans text-2xl text-zinc-900 sm:text-3xl dark:text-white"
>
Text effects with{" "}
<Highlight className="text-black dark:text-white">mouse-follow highlight</Highlight>
</motion.h1>
</HeroHighlight>
</Card>
<Card label="flip-words">
<div className="text-center text-lg text-zinc-700 dark:text-zinc-200">
Animate text like <FlipWords words={["sparkles", "beams", "pins", "marquees"]} />
</div>
</Card>
<Card label="encrypted-text">
<EncryptedText
text="Aceternity, vendored."
className="text-center text-lg text-zinc-800 dark:text-zinc-100"
/>
</Card>
<Card label="evervault-card" className="md:col-span-2">
<div className="w-full max-w-md">
<EvervaultCard text="hover the capsule" />
</div>
</Card>
<Card label="tracing-beam" className="md:col-span-2">
<TracingBeam className="w-full px-2">
<div className="flex w-full flex-col gap-4">
{[
["Beam one", "The beam follows your scroll."],
["Beam two", "It traces between sticky sections."],
["Beam three", "Pairs perfectly with tall content."],
].map(([t, d]) => (
<div
key={t}
className="rounded-xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-800/50"
>
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">{t}</p>
<p className="text-xs text-zinc-500 dark:text-zinc-400">{d}</p>
</div>
))}
</div>
</TracingBeam>
</Card>
</div>
</section>
<section className="flex w-full flex-col items-center gap-6">
<SectionLabel id="inputs" index="04" title="Inputs with built-in motion" />
<Card label="placeholders-and-vanish-input" className="max-w-2xl">
<PlaceholdersAndVanishInput
placeholders={["Search ", "Try a phrase…", "The placeholder vanishes on focus"]}
onChange={() => {}}
onSubmit={() => {}}
/>
</Card>
</section>
</div>
);
}
+121
View File
@@ -0,0 +1,121 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const ThreeDMarquee = ({ images, className }: { images: string[]; className?: string }) => {
// Split the images array into 4 equal parts
const chunkSize = Math.ceil(images.length / 4);
const chunks = Array.from({ length: 4 }, (_, colIndex) => {
const start = colIndex * chunkSize;
return images.slice(start, start + chunkSize);
});
return (
<div
className={cn("mx-auto block h-[600px] overflow-hidden rounded-2xl max-sm:h-100", className)}
>
<div className="flex size-full items-center justify-center">
<div className="size-[1720px] shrink-0 scale-50 sm:scale-75 lg:scale-100">
<div
style={{
transform: "rotateX(55deg) rotateY(0deg) rotateZ(-45deg)",
}}
className="relative top-96 right-[50%] grid size-full origin-top-left grid-cols-4 gap-8 transform-3d"
>
{chunks.map((subarray, colIndex) => (
<motion.div
animate={{ y: colIndex % 2 === 0 ? 100 : -100 }}
transition={{
duration: colIndex % 2 === 0 ? 10 : 15,
repeat: Infinity,
repeatType: "reverse",
}}
key={colIndex + "marquee"}
className="flex flex-col items-start gap-8"
>
<GridLineVertical className="-left-4" offset="80px" />
{subarray.map((image, imageIndex) => (
<div className="relative" key={imageIndex + image}>
<GridLineHorizontal className="-top-4" offset="20px" />
<motion.img
whileHover={{
y: -10,
}}
transition={{
duration: 0.3,
ease: "easeInOut",
}}
key={imageIndex + image}
src={image}
alt={`Image ${imageIndex + 1}`}
className="aspect-[970/700] rounded-lg object-cover ring ring-gray-950/5 hover:shadow-2xl"
width={970}
height={700}
/>
</div>
))}
</motion.div>
))}
</div>
</div>
</div>
</div>
);
};
const GridLineHorizontal = ({ className, offset }: { className?: string; offset?: string }) => {
return (
<div
style={
{
"--background": "#ffffff",
"--color": "rgba(0, 0, 0, 0.2)",
"--height": "1px",
"--width": "5px",
"--fade-stop": "90%",
"--offset": offset || "200px", //-100px if you want to keep the line inside
"--color-dark": "rgba(255, 255, 255, 0.2)",
maskComposite: "exclude",
} as React.CSSProperties
}
className={cn(
"absolute left-[calc(var(--offset)/2*-1)] h-[var(--height)] w-[calc(100%+var(--offset))]",
"bg-[linear-gradient(to_right,var(--color),var(--color)_50%,transparent_0,transparent)]",
"[background-size:var(--width)_var(--height)]",
"[mask:linear-gradient(to_left,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_right,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
"[mask-composite:exclude]",
"z-30",
"dark:bg-[linear-gradient(to_right,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
className
)}
></div>
);
};
const GridLineVertical = ({ className, offset }: { className?: string; offset?: string }) => {
return (
<div
style={
{
"--background": "#ffffff",
"--color": "rgba(0, 0, 0, 0.2)",
"--height": "5px",
"--width": "1px",
"--fade-stop": "90%",
"--offset": offset || "150px", //-100px if you want to keep the line inside
"--color-dark": "rgba(255, 255, 255, 0.2)",
maskComposite: "exclude",
} as React.CSSProperties
}
className={cn(
"absolute top-[calc(var(--offset)/2*-1)] h-[calc(100%+var(--offset))] w-[var(--width)]",
"bg-[linear-gradient(to_bottom,var(--color),var(--color)_50%,transparent_0,transparent)]",
"[background-size:var(--width)_var(--height)]",
"[mask:linear-gradient(to_top,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_bottom,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
"[mask-composite:exclude]",
"z-30",
"dark:bg-[linear-gradient(to_bottom,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
className
)}
></div>
);
};
+154
View File
@@ -0,0 +1,154 @@
"use client";
import React, { useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const PinContainer = ({
children,
title,
href,
className,
containerClassName,
}: {
children: React.ReactNode;
title?: string;
href?: string;
className?: string;
containerClassName?: string;
}) => {
const [transform, setTransform] = useState("translate(-50%,-50%) rotateX(0deg)");
const onMouseEnter = () => {
setTransform("translate(-50%,-50%) rotateX(40deg) scale(0.8)");
};
const onMouseLeave = () => {
setTransform("translate(-50%,-50%) rotateX(0deg) scale(1)");
};
return (
<a
className={cn("group/pin relative z-50 cursor-pointer", containerClassName)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
href={href || "/"}
>
<div
style={{
perspective: "1000px",
transform: "rotateX(70deg) translateZ(0deg)",
}}
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
>
<div
style={{
transform: transform,
}}
className="absolute top-1/2 left-1/2 flex items-start justify-start overflow-hidden rounded-2xl border border-white/[0.1] bg-black p-4 shadow-[0_8px_16px_rgb(0_0_0/0.4)] transition duration-700 group-hover/pin:border-white/[0.2]"
>
<div className={cn("relative z-50", className)}>{children}</div>
</div>
</div>
<PinPerspective title={title} href={href} />
</a>
);
};
export const PinPerspective = ({ title, href }: { title?: string; href?: string }) => {
return (
<motion.div className="pointer-events-none z-[60] flex h-80 w-96 items-center justify-center opacity-0 transition duration-500 group-hover/pin:opacity-100">
<div className="inset-0 -mt-7 h-full w-full flex-none">
<div className="absolute inset-x-0 top-0 flex justify-center">
<a
href={href}
target={"_blank"}
className="relative z-10 flex items-center space-x-2 rounded-full bg-zinc-950 px-4 py-0.5 ring-1 ring-white/10"
>
<span className="relative z-20 inline-block py-0.5 text-xs font-bold text-white">
{title}
</span>
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-emerald-400/90 to-emerald-400/0 transition-opacity duration-500 group-hover/btn:opacity-40"></span>
</a>
</div>
<div
style={{
perspective: "1000px",
transform: "rotateX(70deg) translateZ(0)",
}}
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
>
<>
<motion.div
initial={{
opacity: 0,
scale: 0,
x: "-50%",
y: "-50%",
}}
animate={{
opacity: [0, 1, 0.5, 0],
scale: 1,
z: 0,
}}
transition={{
duration: 6,
repeat: Infinity,
delay: 0,
}}
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
></motion.div>
<motion.div
initial={{
opacity: 0,
scale: 0,
x: "-50%",
y: "-50%",
}}
animate={{
opacity: [0, 1, 0.5, 0],
scale: 1,
z: 0,
}}
transition={{
duration: 6,
repeat: Infinity,
delay: 2,
}}
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
></motion.div>
<motion.div
initial={{
opacity: 0,
scale: 0,
x: "-50%",
y: "-50%",
}}
animate={{
opacity: [0, 1, 0.5, 0],
scale: 1,
z: 0,
}}
transition={{
duration: 6,
repeat: Infinity,
delay: 4,
}}
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
></motion.div>
</>
</div>
<>
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-cyan-500 blur-[2px] group-hover/pin:h-40" />
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-cyan-500 group-hover/pin:h-40" />
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[4px] w-[4px] translate-x-[1.5px] translate-y-[14px] rounded-full bg-cyan-600 blur-[3px]" />
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[2px] w-[2px] translate-x-[0.5px] translate-y-[14px] rounded-full bg-cyan-300" />
</>
</div>
</motion.div>
);
};
+214
View File
@@ -0,0 +1,214 @@
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import React, { ReactNode, createContext, useContext, useEffect, useRef, useState } from "react";
interface ModalContextType {
open: boolean;
setOpen: (open: boolean) => void;
}
const ModalContext = createContext<ModalContextType | undefined>(undefined);
export const ModalProvider = ({ children }: { children: ReactNode }) => {
const [open, setOpen] = useState(false);
return <ModalContext.Provider value={{ open, setOpen }}>{children}</ModalContext.Provider>;
};
export const useModal = () => {
const context = useContext(ModalContext);
if (!context) {
throw new Error("useModal must be used within a ModalProvider");
}
return context;
};
export function Modal({ children }: { children: ReactNode }) {
return <ModalProvider>{children}</ModalProvider>;
}
export const ModalTrigger = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
const { setOpen } = useModal();
return (
<button
className={cn(
"relative overflow-hidden rounded-md px-4 py-2 text-center text-black dark:text-white",
className
)}
onClick={() => setOpen(true)}
>
{children}
</button>
);
};
export const ModalBody = ({ children, className }: { children: ReactNode; className?: string }) => {
const { open } = useModal();
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
}
}, [open]);
const modalRef = useRef<HTMLDivElement>(null);
const { setOpen } = useModal();
useOutsideClick(modalRef, () => setOpen(false));
return (
<AnimatePresence>
{open && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
backdropFilter: "blur(10px)",
}}
exit={{
opacity: 0,
backdropFilter: "blur(0px)",
}}
className="fixed inset-0 z-50 flex h-full w-full items-center justify-center [perspective:800px] [transform-style:preserve-3d]"
>
<Overlay />
<motion.div
ref={modalRef}
className={cn(
"relative z-50 flex max-h-[90%] min-h-[50%] flex-1 flex-col overflow-hidden border border-transparent bg-white md:max-w-[40%] md:rounded-2xl dark:border-neutral-800 dark:bg-neutral-950",
className
)}
initial={{
opacity: 0,
scale: 0.5,
rotateX: 40,
y: 40,
}}
animate={{
opacity: 1,
scale: 1,
rotateX: 0,
y: 0,
}}
exit={{
opacity: 0,
scale: 0.8,
rotateX: 10,
}}
transition={{
type: "spring",
stiffness: 260,
damping: 15,
}}
>
<CloseIcon />
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};
export const ModalContent = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
return <div className={cn("flex flex-1 flex-col p-8 md:p-10", className)}>{children}</div>;
};
export const ModalFooter = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
return (
<div className={cn("flex justify-end bg-gray-100 p-4 dark:bg-neutral-900", className)}>
{children}
</div>
);
};
const Overlay = ({ className }: { className?: string }) => {
return (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
backdropFilter: "blur(10px)",
}}
exit={{
opacity: 0,
backdropFilter: "blur(0px)",
}}
className={`bg-opacity-50 fixed inset-0 z-50 h-full w-full bg-black ${className}`}
></motion.div>
);
};
const CloseIcon = () => {
const { setOpen } = useModal();
return (
<button onClick={() => setOpen(false)} className="group absolute top-4 right-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-black transition duration-200 group-hover:scale-125 group-hover:rotate-3 dark:text-white"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M18 6l-12 12" />
<path d="M6 6l12 12" />
</svg>
</button>
);
};
// Hook to detect clicks outside of a component.
// Add it in a separate file, I've added here for simplicity
export const useOutsideClick = (
ref: React.RefObject<HTMLDivElement | null>,
callback: (event: MouseEvent | TouchEvent) => void
) => {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
// DO NOTHING if the element being clicked is the target element or their children
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
callback(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, callback]);
};
+122
View File
@@ -0,0 +1,122 @@
"use client";
import { useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
type Tab = {
title: string;
value: string;
content?: string | React.ReactNode | any;
};
export const Tabs = ({
tabs: propTabs,
containerClassName,
activeTabClassName,
tabClassName,
contentClassName,
}: {
tabs: Tab[];
containerClassName?: string;
activeTabClassName?: string;
tabClassName?: string;
contentClassName?: string;
}) => {
const [active, setActive] = useState<Tab>(propTabs[0]);
const [tabs, setTabs] = useState<Tab[]>(propTabs);
const moveSelectedTabToTop = (idx: number) => {
const newTabs = [...propTabs];
const selectedTab = newTabs.splice(idx, 1);
newTabs.unshift(selectedTab[0]);
setTabs(newTabs);
setActive(newTabs[0]);
};
const [hovering, setHovering] = useState(false);
return (
<>
<div
className={cn(
"no-visible-scrollbar relative flex w-full max-w-full flex-row items-center justify-start overflow-auto [perspective:1000px] sm:overflow-visible",
containerClassName
)}
>
{propTabs.map((tab, idx) => (
<button
key={tab.title}
onClick={() => {
moveSelectedTabToTop(idx);
}}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
className={cn("relative rounded-full px-4 py-2", tabClassName)}
style={{
transformStyle: "preserve-3d",
}}
>
{active.value === tab.value && (
<motion.div
layoutId="clickedbutton"
transition={{ type: "spring", bounce: 0.3, duration: 0.6 }}
className={cn(
"absolute inset-0 rounded-full bg-gray-200 dark:bg-zinc-800",
activeTabClassName
)}
/>
)}
<span className="relative block text-black dark:text-white">{tab.title}</span>
</button>
))}
</div>
<FadeInDiv
tabs={tabs}
active={active}
key={active.value}
hovering={hovering}
className={cn("mt-32", contentClassName)}
/>
</>
);
};
export const FadeInDiv = ({
className,
tabs,
hovering,
}: {
className?: string;
key?: string;
tabs: Tab[];
active: Tab;
hovering?: boolean;
}) => {
const isActive = (tab: Tab) => {
return tab.value === tabs[0].value;
};
return (
<div className="relative h-full w-full">
{tabs.map((tab, idx) => (
<motion.div
key={tab.value}
layoutId={tab.value}
style={{
scale: 1 - idx * 0.1,
top: hovering ? idx * -50 : 0,
zIndex: -idx,
opacity: idx < 3 ? 1 - idx * 0.1 : 0,
}}
animate={{
y: isActive(tab) ? [0, 40, 0] : 0,
}}
className={cn("absolute top-0 left-0 h-full w-full", className)}
>
{tab.content}
</motion.div>
))}
</div>
);
};
+85
View File
@@ -0,0 +1,85 @@
"use client";
import { useState, useRef } from "react";
import { motion, useTransform, AnimatePresence, useMotionValue, useSpring } from "motion/react";
export const AnimatedTooltip = ({
items,
}: {
items: {
id: number;
name: string;
designation: string;
image: string;
}[];
}) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const springConfig = { stiffness: 100, damping: 15 };
const x = useMotionValue(0);
const animationFrameRef = useRef<number | null>(null);
const rotate = useSpring(useTransform(x, [-100, 100], [-45, 45]), springConfig);
const translateX = useSpring(useTransform(x, [-100, 100], [-50, 50]), springConfig);
const handleMouseMove = (event: any) => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(() => {
const halfWidth = event.target.offsetWidth / 2;
x.set(event.nativeEvent.offsetX - halfWidth);
});
};
return (
<>
{items.map((item) => (
<div
className="group relative -mr-4"
key={item.name}
onMouseEnter={() => setHoveredIndex(item.id)}
onMouseLeave={() => setHoveredIndex(null)}
>
<AnimatePresence>
{hoveredIndex === item.id && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.6 }}
animate={{
opacity: 1,
y: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 260,
damping: 10,
},
}}
exit={{ opacity: 0, y: 20, scale: 0.6 }}
style={{
translateX: translateX,
rotate: rotate,
whiteSpace: "nowrap",
}}
className="absolute -top-16 left-1/2 z-50 flex -translate-x-1/2 flex-col items-center justify-center rounded-md bg-black px-4 py-2 text-xs shadow-xl"
>
<div className="absolute inset-x-10 -bottom-px z-30 h-px w-[20%] bg-gradient-to-r from-transparent via-emerald-500 to-transparent" />
<div className="absolute -bottom-px left-10 z-30 h-px w-[40%] bg-gradient-to-r from-transparent via-sky-500 to-transparent" />
<div className="relative z-30 text-base font-bold text-white">{item.name}</div>
<div className="text-xs text-white">{item.designation}</div>
</motion.div>
)}
</AnimatePresence>
<img
onMouseMove={handleMouseMove}
height={100}
width={100}
src={item.image}
alt={item.name}
className="relative !m-0 h-14 w-14 rounded-full border-2 border-white object-cover object-top !p-0 transition duration-500 group-hover:z-30 group-hover:scale-105"
/>
</div>
))}
</>
);
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+69
View File
@@ -0,0 +1,69 @@
"use client";
import React from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const BoxesCore = ({ className, ...rest }: { className?: string }) => {
const rows = new Array(150).fill(1);
const cols = new Array(100).fill(1);
const colors = [
"#93c5fd",
"#f9a8d4",
"#86efac",
"#fde047",
"#fca5a5",
"#d8b4fe",
"#93c5fd",
"#a5b4fc",
"#c4b5fd",
];
const getRandomColor = () => {
return colors[Math.floor(Math.random() * colors.length)];
};
return (
<div
style={{
transform: `translate(-40%,-60%) skewX(-48deg) skewY(14deg) scale(0.675) rotate(0deg) translateZ(0)`,
}}
className={cn(
"absolute -top-1/4 left-1/4 z-0 flex h-full w-full -translate-x-1/2 -translate-y-1/2 p-4",
className
)}
{...rest}
>
{rows.map((_, i) => (
<motion.div key={`row` + i} className="relative h-8 w-16 border-l border-slate-700">
{cols.map((_, j) => (
<motion.div
whileHover={{
backgroundColor: `${getRandomColor()}`,
transition: { duration: 0 },
}}
animate={{
transition: { duration: 2 },
}}
key={`col` + j}
className="relative h-8 w-16 border-t border-r border-slate-700"
>
{j % 2 === 0 && i % 2 === 0 ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="pointer-events-none absolute -top-[14px] -left-[22px] h-6 w-10 stroke-[1px] text-slate-700"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
</svg>
) : null}
</motion.div>
))}
</motion.div>
))}
</div>
);
};
export const Boxes = React.memo(BoxesCore);
@@ -0,0 +1,171 @@
"use client";
import { cn } from "@/lib/utils";
import { useEffect, useRef, useState } from "react";
export const BackgroundGradientAnimation = ({
gradientBackgroundStart = "rgb(108, 0, 162)",
gradientBackgroundEnd = "rgb(0, 17, 82)",
firstColor = "18, 113, 255",
secondColor = "221, 74, 255",
thirdColor = "100, 220, 255",
fourthColor = "200, 50, 50",
fifthColor = "180, 180, 50",
pointerColor = "140, 100, 255",
size = "80%",
blendingValue = "hard-light",
children,
className,
interactive = true,
containerClassName,
}: {
gradientBackgroundStart?: string;
gradientBackgroundEnd?: string;
firstColor?: string;
secondColor?: string;
thirdColor?: string;
fourthColor?: string;
fifthColor?: string;
pointerColor?: string;
size?: string;
blendingValue?: string;
children?: React.ReactNode;
className?: string;
interactive?: boolean;
containerClassName?: string;
}) => {
const interactiveRef = useRef<HTMLDivElement>(null);
const [curX, setCurX] = useState(0);
const [curY, setCurY] = useState(0);
const [tgX, setTgX] = useState(0);
const [tgY, setTgY] = useState(0);
useEffect(() => {
document.body.style.setProperty("--gradient-background-start", gradientBackgroundStart);
document.body.style.setProperty("--gradient-background-end", gradientBackgroundEnd);
document.body.style.setProperty("--first-color", firstColor);
document.body.style.setProperty("--second-color", secondColor);
document.body.style.setProperty("--third-color", thirdColor);
document.body.style.setProperty("--fourth-color", fourthColor);
document.body.style.setProperty("--fifth-color", fifthColor);
document.body.style.setProperty("--pointer-color", pointerColor);
document.body.style.setProperty("--size", size);
document.body.style.setProperty("--blending-value", blendingValue);
}, []);
useEffect(() => {
function move() {
if (!interactiveRef.current) {
return;
}
setCurX(curX + (tgX - curX) / 20);
setCurY(curY + (tgY - curY) / 20);
interactiveRef.current.style.transform = `translate(${Math.round(
curX
)}px, ${Math.round(curY)}px)`;
}
move();
}, [tgX, tgY]);
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (interactiveRef.current) {
const rect = interactiveRef.current.getBoundingClientRect();
setTgX(event.clientX - rect.left);
setTgY(event.clientY - rect.top);
}
};
const [isSafari, setIsSafari] = useState(false);
useEffect(() => {
setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent));
}, []);
return (
<div
className={cn(
"relative top-0 left-0 h-screen w-screen overflow-hidden bg-[linear-gradient(40deg,var(--gradient-background-start),var(--gradient-background-end))]",
containerClassName
)}
>
<svg className="hidden">
<defs>
<filter id="blurMe">
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
<feColorMatrix
in="blur"
mode="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -8"
result="goo"
/>
<feBlend in="SourceGraphic" in2="goo" />
</filter>
</defs>
</svg>
<div className={cn("", className)}>{children}</div>
<div
className={cn(
"gradients-container h-full w-full blur-lg",
isSafari ? "blur-2xl" : "[filter:url(#blurMe)_blur(40px)]"
)}
>
<div
className={cn(
`absolute [background:radial-gradient(circle_at_center,_var(--first-color)_0,_var(--first-color)_50%)_no-repeat]`,
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
`[transform-origin:center_center]`,
`animate-first`,
`opacity-100`
)}
></div>
<div
className={cn(
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--second-color),_0.8)_0,_rgba(var(--second-color),_0)_50%)_no-repeat]`,
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
`[transform-origin:calc(50%-400px)]`,
`animate-second`,
`opacity-100`
)}
></div>
<div
className={cn(
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--third-color),_0.8)_0,_rgba(var(--third-color),_0)_50%)_no-repeat]`,
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
`[transform-origin:calc(50%+400px)]`,
`animate-third`,
`opacity-100`
)}
></div>
<div
className={cn(
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--fourth-color),_0.8)_0,_rgba(var(--fourth-color),_0)_50%)_no-repeat]`,
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
`[transform-origin:calc(50%-200px)]`,
`animate-fourth`,
`opacity-70`
)}
></div>
<div
className={cn(
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--fifth-color),_0.8)_0,_rgba(var(--fifth-color),_0)_50%)_no-repeat]`,
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
`[transform-origin:calc(50%-800px)_calc(50%+800px)]`,
`animate-fifth`,
`opacity-100`
)}
></div>
{interactive && (
<div
ref={interactiveRef}
onMouseMove={handleMouseMove}
className={cn(
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--pointer-color),_0.8)_0,_rgba(var(--pointer-color),_0)_50%)_no-repeat]`,
`-top-1/2 -left-1/2 h-full w-full [mix-blend-mode:var(--blending-value)]`,
`opacity-70`
)}
></div>
)}
</div>
</div>
);
};
+72
View File
@@ -0,0 +1,72 @@
import { cn } from "@/lib/utils";
import React from "react";
import { motion } from "motion/react";
export const BackgroundGradient = ({
children,
className,
containerClassName,
animate = true,
}: {
children?: React.ReactNode;
className?: string;
containerClassName?: string;
animate?: boolean;
}) => {
const variants = {
initial: {
backgroundPosition: "0 50%",
},
animate: {
backgroundPosition: ["0, 50%", "100% 50%", "0 50%"],
},
};
return (
<div className={cn("group relative p-[4px]", containerClassName)}>
<motion.div
variants={animate ? variants : undefined}
initial={animate ? "initial" : undefined}
animate={animate ? "animate" : undefined}
transition={
animate
? {
duration: 5,
repeat: Infinity,
repeatType: "reverse",
}
: undefined
}
style={{
backgroundSize: animate ? "400% 400%" : undefined,
}}
className={cn(
"absolute inset-0 z-[1] rounded-3xl opacity-60 blur-xl transition duration-500 will-change-transform group-hover:opacity-100",
"bg-[radial-gradient(circle_farthest-side_at_0_100%,#00ccb1,transparent),radial-gradient(circle_farthest-side_at_100%_0,#7b61ff,transparent),radial-gradient(circle_farthest-side_at_100%_100%,#ffc414,transparent),radial-gradient(circle_farthest-side_at_0_0,#1ca0fb,#141316)]"
)}
/>
<motion.div
variants={animate ? variants : undefined}
initial={animate ? "initial" : undefined}
animate={animate ? "animate" : undefined}
transition={
animate
? {
duration: 5,
repeat: Infinity,
repeatType: "reverse",
}
: undefined
}
style={{
backgroundSize: animate ? "400% 400%" : undefined,
}}
className={cn(
"absolute inset-0 z-[1] rounded-3xl will-change-transform",
"bg-[radial-gradient(circle_farthest-side_at_0_100%,#00ccb1,transparent),radial-gradient(circle_farthest-side_at_100%_0,#7b61ff,transparent),radial-gradient(circle_farthest-side_at_100%_100%,#ffc414,transparent),radial-gradient(circle_farthest-side_at_0_0,#1ca0fb,#141316)]"
)}
/>
<div className={cn("relative z-10", className)}>{children}</div>
</div>
);
};
+54
View File
@@ -0,0 +1,54 @@
import { cn } from "@/lib/utils";
export const BentoGrid = ({
className,
children,
}: {
className?: string;
children?: React.ReactNode;
}) => {
return (
<div
className={cn(
"mx-auto grid max-w-7xl grid-cols-1 gap-4 md:auto-rows-[18rem] md:grid-cols-3",
className
)}
>
{children}
</div>
);
};
export const BentoGridItem = ({
className,
title,
description,
header,
icon,
}: {
className?: string;
title?: string | React.ReactNode;
description?: string | React.ReactNode;
header?: React.ReactNode;
icon?: React.ReactNode;
}) => {
return (
<div
className={cn(
"group/bento row-span-1 flex flex-col justify-between space-y-4 rounded-xl border border-neutral-200 bg-white p-4 shadow-input transition duration-200 hover:shadow-xl dark:border-white/[0.2] dark:bg-black dark:shadow-none",
className
)}
>
{header}
<div className="transition duration-200 group-hover/bento:translate-x-2">
{icon}
<div className="mt-2 mb-2 font-sans font-bold text-neutral-600 dark:text-neutral-200">
{title}
</div>
<div className="font-sans text-xs font-normal text-neutral-600 dark:text-neutral-300">
{description}
</div>
</div>
</div>
);
};
+99
View File
@@ -0,0 +1,99 @@
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
export const HoverEffect = ({
items,
className,
}: {
items: {
title: string;
description: string;
link: string;
}[];
className?: string;
}) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
return (
<div className={cn("grid grid-cols-1 py-10 md:grid-cols-2 lg:grid-cols-3", className)}>
{items.map((item, idx) => (
<a
href={item?.link}
key={item?.link}
className="group relative block h-full w-full p-2"
onMouseEnter={() => setHoveredIndex(idx)}
onMouseLeave={() => setHoveredIndex(null)}
>
<AnimatePresence>
{hoveredIndex === idx && (
<motion.span
className="absolute inset-0 block h-full w-full rounded-3xl bg-neutral-200 dark:bg-slate-800/[0.8]"
layoutId="hoverBackground"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { duration: 0.15 },
}}
exit={{
opacity: 0,
transition: { duration: 0.15, delay: 0.2 },
}}
/>
)}
</AnimatePresence>
<Card>
<CardTitle>{item.title}</CardTitle>
<CardDescription>{item.description}</CardDescription>
</Card>
</a>
))}
</div>
);
};
export const Card = ({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) => {
return (
<div
className={cn(
"relative z-20 h-full w-full overflow-hidden rounded-2xl border border-transparent bg-black p-4 group-hover:border-slate-700 dark:border-white/[0.2]",
className
)}
>
<div className="relative z-50">
<div className="p-4">{children}</div>
</div>
</div>
);
};
export const CardTitle = ({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) => {
return (
<h4 className={cn("mt-4 font-bold tracking-wide text-zinc-100", className)}>{children}</h4>
);
};
export const CardDescription = ({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) => {
return (
<p className={cn("mt-8 text-sm leading-relaxed tracking-wide text-zinc-400", className)}>
{children}
</p>
);
};
+61
View File
@@ -0,0 +1,61 @@
"use client";
import { useMotionValue, motion, useMotionTemplate } from "motion/react";
import React, { MouseEvent as ReactMouseEvent, useState } from "react";
import { cn } from "@/lib/utils";
export const CardSpotlight = ({
children,
radius = 350,
color = "#262626",
className,
...props
}: {
radius?: number;
color?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>) => {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
function handleMouseMove({ currentTarget, clientX, clientY }: ReactMouseEvent<HTMLDivElement>) {
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top);
}
const [isHovering, setIsHovering] = useState(false);
const handleMouseEnter = () => setIsHovering(true);
const handleMouseLeave = () => setIsHovering(false);
return (
<div
className={cn(
"group/spotlight relative rounded-md border border-neutral-800 bg-black p-10 dark:border-neutral-800",
className
)}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
>
<motion.div
className="pointer-events-none absolute -inset-px z-0 rounded-md opacity-0 transition duration-300 group-hover/spotlight:opacity-100"
style={{
backgroundColor: color,
maskImage: useMotionTemplate`
radial-gradient(
${radius}px circle at ${mouseX}px ${mouseY}px,
white,
transparent 80%
)
`,
}}
>
{isHovering && (
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_var(--spotlight-x,50%)_var(--spotlight-y,50%),rgba(59,130,246,0.35),transparent_60%)]" />
)}
</motion.div>
{children}
</div>
);
};
+65
View File
@@ -0,0 +1,65 @@
"use client";
import { useEffect, useState } from "react";
import { motion } from "motion/react";
type Card = {
id: number;
name: string;
designation: string;
content: React.ReactNode;
};
export const CardStack = ({
items,
offset,
scaleFactor,
}: {
items: Card[];
offset?: number;
scaleFactor?: number;
}) => {
const CARD_OFFSET = offset || 10;
const SCALE_FACTOR = scaleFactor || 0.06;
const [cards, setCards] = useState<Card[]>(items);
useEffect(() => {
const interval = setInterval(() => {
setCards((prevCards: Card[]) => {
const newArray = [...prevCards]; // create a copy of the array
newArray.unshift(newArray.pop()!); // move the last element to the front
return newArray;
});
}, 5000);
return () => clearInterval(interval);
}, []);
return (
<div className="relative h-60 w-60 md:h-60 md:w-96">
{cards.map((card, index) => {
return (
<motion.div
key={card.id}
className="absolute flex h-60 w-60 flex-col justify-between rounded-3xl border border-neutral-200 bg-white p-4 shadow-xl shadow-black/[0.1] md:h-60 md:w-96 dark:border-white/[0.1] dark:bg-black dark:shadow-white/[0.05]"
style={{
transformOrigin: "top center",
}}
animate={{
top: index * -CARD_OFFSET,
scale: 1 - index * SCALE_FACTOR, // decrease scale for cards that are behind
zIndex: cards.length - index, // decrease z-index for the cards that are behind
}}
>
<div className="font-normal text-neutral-700 dark:text-neutral-200">{card.content}</div>
<div>
<p className="font-medium text-neutral-500 dark:text-white">{card.name}</p>
<p className="font-normal text-neutral-400 dark:text-neutral-200">
{card.designation}
</p>
</div>
</motion.div>
);
})}
</div>
);
};
@@ -0,0 +1,95 @@
"use client";
import React, { useRef } from "react";
import { useScroll, useTransform, motion, MotionValue } from "motion/react";
export const ContainerScroll = ({
titleComponent,
children,
}: {
titleComponent: string | React.ReactNode;
children: React.ReactNode;
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
});
const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth <= 768);
};
checkMobile();
window.addEventListener("resize", checkMobile);
return () => {
window.removeEventListener("resize", checkMobile);
};
}, []);
const scaleDimensions = () => {
return isMobile ? [0.7, 0.9] : [1.05, 1];
};
const rotate = useTransform(scrollYProgress, [0, 1], [20, 0]);
const scale = useTransform(scrollYProgress, [0, 1], scaleDimensions());
const translate = useTransform(scrollYProgress, [0, 1], [0, -100]);
return (
<div
className="relative flex h-[60rem] items-center justify-center p-2 md:h-[80rem] md:p-20"
ref={containerRef}
>
<div
className="relative w-full py-10 md:py-40"
style={{
perspective: "1000px",
}}
>
<Header translate={translate} titleComponent={titleComponent} />
<Card rotate={rotate} translate={translate} scale={scale}>
{children}
</Card>
</div>
</div>
);
};
export const Header = ({ translate, titleComponent }: any) => {
return (
<motion.div
style={{
translateY: translate,
}}
className="div mx-auto max-w-5xl text-center"
>
{titleComponent}
</motion.div>
);
};
export const Card = ({
rotate,
scale,
children,
}: {
rotate: MotionValue<number>;
scale: MotionValue<number>;
translate: MotionValue<number>;
children: React.ReactNode;
}) => {
return (
<motion.div
style={{
rotateX: rotate,
scale,
boxShadow:
"0 0 #0000004d, 0 9px 20px #0000004a, 0 37px 37px #00000042, 0 84px 50px #00000026, 0 149px 60px #0000000a, 0 233px 65px #00000003",
}}
className="mx-auto -mt-12 h-[30rem] w-full max-w-5xl rounded-[30px] border-4 border-[#6C6C6C] bg-[#222222] p-2 shadow-2xl md:h-[40rem] md:p-6"
>
<div className="h-full w-full overflow-hidden rounded-2xl bg-gray-100 md:rounded-2xl md:p-4 dark:bg-zinc-900">
{children}
</div>
</motion.div>
);
};
+219
View File
@@ -0,0 +1,219 @@
"use client";
import React, { useEffect, useId, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { useRef } from "react";
import { cn } from "@/lib/utils";
import { SparklesCore } from "@/components/ui/sparkles";
export const Cover = ({
children,
className,
}: {
children?: React.ReactNode;
className?: string;
}) => {
const [hovered, setHovered] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const [containerWidth, setContainerWidth] = useState(0);
const [beamPositions, setBeamPositions] = useState<number[]>([]);
useEffect(() => {
if (ref.current) {
setContainerWidth(ref.current?.clientWidth ?? 0);
const height = ref.current?.clientHeight ?? 0;
const numberOfBeams = Math.floor(height / 10); // Adjust the divisor to control the spacing
const positions = Array.from(
{ length: numberOfBeams },
(_, i) => (i + 1) * (height / (numberOfBeams + 1))
);
setBeamPositions(positions);
}
}, [ref.current]);
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
ref={ref}
className="group/cover relative inline-block rounded-sm bg-neutral-100 px-2 py-2 transition duration-200 hover:bg-neutral-900 dark:bg-neutral-900"
>
<AnimatePresence>
{hovered && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
opacity: {
duration: 0.2,
},
}}
className="absolute inset-0 h-full w-full overflow-hidden"
>
<motion.div
animate={{
translateX: ["-50%", "0%"],
}}
transition={{
translateX: {
duration: 10,
ease: "linear",
repeat: Infinity,
},
}}
className="flex h-full w-[200%]"
>
<SparklesCore
background="transparent"
minSize={0.4}
maxSize={1}
particleDensity={500}
className="h-full w-full"
particleColor="#FFFFFF"
/>
<SparklesCore
background="transparent"
minSize={0.4}
maxSize={1}
particleDensity={500}
className="h-full w-full"
particleColor="#FFFFFF"
/>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{beamPositions.map((position, index) => (
<Beam
key={index}
hovered={hovered}
duration={Math.random() * 2 + 1}
delay={Math.random() * 2 + 1}
width={containerWidth}
style={{
top: `${position}px`,
}}
/>
))}
<motion.span
key={String(hovered)}
animate={{
scale: hovered ? 0.8 : 1,
x: hovered ? [0, -30, 30, -30, 30, 0] : 0,
y: hovered ? [0, 30, -30, 30, -30, 0] : 0,
}}
exit={{
filter: "none",
scale: 1,
x: 0,
y: 0,
}}
transition={{
duration: 0.2,
x: {
duration: 0.2,
repeat: Infinity,
repeatType: "loop",
},
y: {
duration: 0.2,
repeat: Infinity,
repeatType: "loop",
},
scale: {
duration: 0.2,
},
filter: {
duration: 0.2,
},
}}
className={cn(
"relative z-20 inline-block text-neutral-900 transition duration-200 group-hover/cover:text-white dark:text-white",
className
)}
>
{children}
</motion.span>
<CircleIcon className="absolute -top-[2px] -right-[2px]" />
<CircleIcon className="absolute -right-[2px] -bottom-[2px]" />
<CircleIcon className="absolute -top-[2px] -left-[2px]" />
<CircleIcon className="absolute -bottom-[2px] -left-[2px]" />
</div>
);
};
export const Beam = ({
className,
delay,
duration,
hovered,
width = 600,
...svgProps
}: {
className?: string;
delay?: number;
duration?: number;
hovered?: boolean;
width?: number;
} & React.ComponentProps<typeof motion.svg>) => {
const id = useId();
return (
<motion.svg
width={width ?? "600"}
height="1"
viewBox={`0 0 ${width ?? "600"} 1`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn("absolute inset-x-0 w-full", className)}
{...svgProps}
>
<motion.path d={`M0 0.5H${width ?? "600"}`} stroke={`url(#svgGradient-${id})`} />
<defs>
<motion.linearGradient
id={`svgGradient-${id}`}
key={String(hovered)}
gradientUnits="userSpaceOnUse"
initial={{
x1: "0%",
x2: hovered ? "-10%" : "-5%",
y1: 0,
y2: 0,
}}
animate={{
x1: "110%",
x2: hovered ? "100%" : "105%",
y1: 0,
y2: 0,
}}
transition={{
duration: hovered ? 0.5 : (duration ?? 2),
ease: "linear",
repeat: Infinity,
delay: hovered ? Math.random() * (1 - 0.2) + 0.2 : 0,
repeatDelay: hovered ? Math.random() * (2 - 1) + 1 : (delay ?? 1),
}}
>
<stop stopColor="#2EB9DF" stopOpacity="0" />
<stop stopColor="#3b82f6" />
<stop offset="1" stopColor="#3b82f6" stopOpacity="0" />
</motion.linearGradient>
</defs>
</motion.svg>
);
};
export const CircleIcon = ({ className }: { className?: string }) => {
return (
<div
className={cn(
`group pointer-events-none h-2 w-2 animate-pulse rounded-full bg-neutral-600 opacity-20 group-hover/cover:hidden group-hover/cover:bg-white group-hover/cover:opacity-100 dark:bg-white`,
className
)}
></div>
);
};
+156
View File
@@ -0,0 +1,156 @@
"use client";
import { useRef, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/lib/utils";
export const DirectionAwareHover = ({
imageUrl,
children,
childrenClassName,
imageClassName,
className,
}: {
imageUrl: string;
children: React.ReactNode | string;
childrenClassName?: string;
imageClassName?: string;
className?: string;
}) => {
const ref = useRef<HTMLDivElement>(null);
const [direction, setDirection] = useState<"top" | "bottom" | "left" | "right" | string>("left");
const handleMouseEnter = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (!ref.current) return;
const direction = getDirection(event, ref.current);
console.log("direction", direction);
switch (direction) {
case 0:
setDirection("top");
break;
case 1:
setDirection("right");
break;
case 2:
setDirection("bottom");
break;
case 3:
setDirection("left");
break;
default:
setDirection("left");
break;
}
};
const getDirection = (ev: React.MouseEvent<HTMLDivElement, MouseEvent>, obj: HTMLElement) => {
const { width: w, height: h, left, top } = obj.getBoundingClientRect();
const x = ev.clientX - left - (w / 2) * (w > h ? h / w : 1);
const y = ev.clientY - top - (h / 2) * (h > w ? w / h : 1);
const d = Math.round(Math.atan2(y, x) / 1.57079633 + 5) % 4;
return d;
};
return (
<motion.div
onMouseEnter={handleMouseEnter}
ref={ref}
className={cn(
"group/card relative h-60 w-60 overflow-hidden rounded-lg bg-transparent md:h-96 md:w-96",
className
)}
>
<AnimatePresence mode="wait">
<motion.div
className="relative h-full w-full"
initial="initial"
whileHover={direction}
exit="exit"
>
<motion.div className="absolute inset-0 z-10 hidden h-full w-full bg-black/40 transition duration-500 group-hover/card:block" />
<motion.div
variants={variants}
className="relative h-full w-full bg-gray-50 dark:bg-black"
transition={{
duration: 0.2,
ease: "easeOut",
}}
>
<img
alt="image"
className={cn("h-full w-full scale-[1.15] object-cover", imageClassName)}
width="1000"
height="1000"
src={imageUrl}
/>
</motion.div>
<motion.div
variants={textVariants}
transition={{
duration: 0.5,
ease: "easeOut",
}}
className={cn("absolute bottom-4 left-4 z-40 text-white", childrenClassName)}
>
{children}
</motion.div>
</motion.div>
</AnimatePresence>
</motion.div>
);
};
const variants = {
initial: {
x: 0,
},
exit: {
x: 0,
y: 0,
},
top: {
y: 20,
},
bottom: {
y: -20,
},
left: {
x: 20,
},
right: {
x: -20,
},
};
const textVariants = {
initial: {
y: 0,
x: 0,
opacity: 0,
},
exit: {
y: 0,
x: 0,
opacity: 0,
},
top: {
y: -20,
opacity: 1,
},
bottom: {
y: 2,
opacity: 1,
},
left: {
x: -2,
opacity: 1,
},
right: {
x: 20,
opacity: 1,
},
};
+141
View File
@@ -0,0 +1,141 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { motion, useInView } from "motion/react";
import { cn } from "@/lib/utils";
type EncryptedTextProps = {
text: string;
className?: string;
/**
* Time in milliseconds between revealing each subsequent real character.
* Lower is faster. Defaults to 50ms per character.
*/
revealDelayMs?: number;
/** Optional custom character set to use for the gibberish effect. */
charset?: string;
/**
* Time in milliseconds between gibberish flips for unrevealed characters.
* Lower is more jittery. Defaults to 50ms.
*/
flipDelayMs?: number;
/** CSS class for styling the encrypted/scrambled characters */
encryptedClassName?: string;
/** CSS class for styling the revealed characters */
revealedClassName?: string;
};
const DEFAULT_CHARSET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-={}[];:,.<>/?";
function generateRandomCharacter(charset: string): string {
const index = Math.floor(Math.random() * charset.length);
return charset.charAt(index);
}
function generateGibberishPreservingSpaces(original: string, charset: string): string {
if (!original) return "";
let result = "";
for (let i = 0; i < original.length; i += 1) {
const ch = original[i];
result += ch === " " ? " " : generateRandomCharacter(charset);
}
return result;
}
export const EncryptedText: React.FC<EncryptedTextProps> = ({
text,
className,
revealDelayMs = 50,
charset = DEFAULT_CHARSET,
flipDelayMs = 50,
encryptedClassName,
revealedClassName,
}) => {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
const [revealCount, setRevealCount] = useState<number>(0);
const animationFrameRef = useRef<number | null>(null);
const startTimeRef = useRef<number>(0);
const lastFlipTimeRef = useRef<number>(0);
const scrambleCharsRef = useRef<string[]>(
text ? generateGibberishPreservingSpaces(text, charset).split("") : []
);
useEffect(() => {
if (!isInView) return;
// Reset state for a fresh animation whenever dependencies change
const initial = text ? generateGibberishPreservingSpaces(text, charset) : "";
scrambleCharsRef.current = initial.split("");
startTimeRef.current = performance.now();
lastFlipTimeRef.current = startTimeRef.current;
setRevealCount(0);
let isCancelled = false;
const update = (now: number) => {
if (isCancelled) return;
const elapsedMs = now - startTimeRef.current;
const totalLength = text.length;
const currentRevealCount = Math.min(
totalLength,
Math.floor(elapsedMs / Math.max(1, revealDelayMs))
);
setRevealCount(currentRevealCount);
if (currentRevealCount >= totalLength) {
return;
}
// Re-randomize unrevealed scramble characters on an interval
const timeSinceLastFlip = now - lastFlipTimeRef.current;
if (timeSinceLastFlip >= Math.max(0, flipDelayMs)) {
for (let index = 0; index < totalLength; index += 1) {
if (index >= currentRevealCount) {
if (text[index] !== " ") {
scrambleCharsRef.current[index] = generateRandomCharacter(charset);
} else {
scrambleCharsRef.current[index] = " ";
}
}
}
lastFlipTimeRef.current = now;
}
animationFrameRef.current = requestAnimationFrame(update);
};
animationFrameRef.current = requestAnimationFrame(update);
return () => {
isCancelled = true;
if (animationFrameRef.current !== null) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [isInView, text, revealDelayMs, charset, flipDelayMs]);
if (!text) return null;
return (
<motion.span ref={ref} className={cn(className)} aria-label={text} role="text">
{text.split("").map((char, index) => {
const isRevealed = index < revealCount;
const displayChar = isRevealed
? char
: char === " "
? " "
: (scrambleCharsRef.current[index] ?? generateRandomCharacter(charset));
return (
<span key={index} className={cn(isRevealed ? revealedClassName : encryptedClassName)}>
{displayChar}
</span>
);
})}
</motion.span>
);
};
+96
View File
@@ -0,0 +1,96 @@
"use client";
import { useMotionValue } from "motion/react";
import { useState, useEffect } from "react";
import { useMotionTemplate, motion } from "motion/react";
import { cn } from "@/lib/utils";
export const EvervaultCard = ({ text, className }: { text?: string; className?: string }) => {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const [randomString, setRandomString] = useState("");
useEffect(() => {
const str = generateRandomString(1500);
setRandomString(str);
}, []);
function onMouseMove({ currentTarget, clientX, clientY }: any) {
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top);
const str = generateRandomString(1500);
setRandomString(str);
}
return (
<div
className={cn(
"relative flex aspect-square h-full w-full items-center justify-center bg-transparent p-0.5",
className
)}
>
<div
onMouseMove={onMouseMove}
className="group/card relative flex h-full w-full items-center justify-center overflow-hidden rounded-3xl bg-transparent"
>
<CardPattern mouseX={mouseX} mouseY={mouseY} randomString={randomString} />
<div className="relative z-10 flex items-center justify-center">
<div className="relative flex h-44 w-44 items-center justify-center rounded-full text-4xl font-bold text-white">
<div className="absolute h-full w-full rounded-full bg-white/[0.8] blur-sm dark:bg-black/[0.8]" />
<span className="z-20 text-black dark:text-white">{text}</span>
</div>
</div>
</div>
</div>
);
};
export function CardPattern({ mouseX, mouseY, randomString }: any) {
const maskImage = useMotionTemplate`radial-gradient(250px at ${mouseX}px ${mouseY}px, white, transparent)`;
const style = { maskImage, WebkitMaskImage: maskImage };
return (
<div className="pointer-events-none">
<div className="absolute inset-0 rounded-2xl [mask-image:linear-gradient(white,transparent)] group-hover/card:opacity-50"></div>
<motion.div
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-green-500 to-blue-700 opacity-0 backdrop-blur-xl transition duration-500 group-hover/card:opacity-100"
style={style}
/>
<motion.div
className="absolute inset-0 rounded-2xl opacity-0 mix-blend-overlay group-hover/card:opacity-100"
style={style}
>
<p className="absolute inset-x-0 h-full font-mono text-xs font-bold break-words whitespace-pre-wrap text-white transition duration-500">
{randomString}
</p>
</motion.div>
</div>
);
}
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
export const generateRandomString = (length: number) => {
let result = "";
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
};
export const Icon = ({ className, ...rest }: any) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className={className}
{...rest}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
</svg>
);
};
+98
View File
@@ -0,0 +1,98 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/lib/utils";
export const FlipWords = ({
words,
duration = 3000,
className,
}: {
words: string[];
duration?: number;
className?: string;
}) => {
const [currentWord, setCurrentWord] = useState(words[0]);
const [isAnimating, setIsAnimating] = useState<boolean>(false);
// thanks for the fix Julian - https://github.com/Julian-AT
const startAnimation = useCallback(() => {
const word = words[words.indexOf(currentWord) + 1] || words[0];
setCurrentWord(word);
setIsAnimating(true);
}, [currentWord, words]);
useEffect(() => {
if (!isAnimating)
setTimeout(() => {
startAnimation();
}, duration);
}, [isAnimating, duration, startAnimation]);
return (
<AnimatePresence
onExitComplete={() => {
setIsAnimating(false);
}}
>
<motion.div
initial={{
opacity: 0,
y: 10,
}}
animate={{
opacity: 1,
y: 0,
}}
transition={{
type: "spring",
stiffness: 100,
damping: 10,
}}
exit={{
opacity: 0,
y: -40,
x: 40,
filter: "blur(8px)",
scale: 2,
position: "absolute",
}}
className={cn(
"relative z-10 inline-block px-2 text-left text-neutral-900 dark:text-neutral-100",
className
)}
key={currentWord}
>
{/* edit suggested by Sajal: https://x.com/DewanganSajal */}
{currentWord.split(" ").map((word, wordIndex) => (
<motion.span
key={word + wordIndex}
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{
delay: wordIndex * 0.3,
duration: 0.3,
}}
className="inline-block whitespace-nowrap"
>
{word.split("").map((letter, letterIndex) => (
<motion.span
key={word + letterIndex}
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{
delay: wordIndex * 0.3 + letterIndex * 0.05,
duration: 0.2,
}}
className="inline-block"
>
{letter}
</motion.span>
))}
<span className="inline-block">&nbsp;</span>
</motion.span>
))}
</motion.div>
</AnimatePresence>
);
};
+196
View File
@@ -0,0 +1,196 @@
"use client";
/**
* Note: Use position fixed according to your needs
* Desktop navbar is better positioned at the bottom
* Mobile navbar is better positioned at bottom right.
**/
import { cn } from "@/lib/utils";
import { IconLayoutNavbarCollapse } from "@tabler/icons-react";
import {
AnimatePresence,
MotionValue,
motion,
useMotionValue,
useSpring,
useTransform,
} from "motion/react";
import { useRef, useState } from "react";
export const FloatingDock = ({
items,
desktopClassName,
mobileClassName,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
desktopClassName?: string;
mobileClassName?: string;
}) => {
return (
<>
<FloatingDockDesktop items={items} className={desktopClassName} />
<FloatingDockMobile items={items} className={mobileClassName} />
</>
);
};
const FloatingDockMobile = ({
items,
className,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
className?: string;
}) => {
const [open, setOpen] = useState(false);
return (
<div className={cn("relative block md:hidden", className)}>
<AnimatePresence>
{open && (
<motion.div
layoutId="nav"
className="absolute inset-x-0 bottom-full mb-2 flex flex-col gap-2"
>
{items.map((item, idx) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 10 }}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: 10,
transition: {
delay: idx * 0.05,
},
}}
transition={{ delay: (items.length - 1 - idx) * 0.05 }}
>
<a
href={item.href}
key={item.title}
className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 dark:bg-neutral-900"
>
<div className="h-4 w-4">{item.icon}</div>
</a>
</motion.div>
))}
</motion.div>
)}
</AnimatePresence>
<button
onClick={() => setOpen(!open)}
className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 dark:bg-neutral-800"
>
<IconLayoutNavbarCollapse className="h-5 w-5 text-neutral-500 dark:text-neutral-400" />
</button>
</div>
);
};
const FloatingDockDesktop = ({
items,
className,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
className?: string;
}) => {
const mouseX = useMotionValue(Infinity);
return (
<motion.div
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
className={cn(
"mx-auto hidden h-16 items-end gap-4 rounded-2xl bg-gray-50 px-4 pb-3 md:flex dark:bg-neutral-900",
className
)}
>
{items.map((item) => (
<IconContainer mouseX={mouseX} key={item.title} {...item} />
))}
</motion.div>
);
};
function IconContainer({
mouseX,
title,
icon,
href,
}: {
mouseX: MotionValue;
title: string;
icon: React.ReactNode;
href: string;
}) {
const ref = useRef<HTMLDivElement>(null);
const distance = useTransform(mouseX, (val) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
const widthTransform = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
const heightTransform = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
const widthTransformIcon = useTransform(distance, [-150, 0, 150], [20, 40, 20]);
const heightTransformIcon = useTransform(distance, [-150, 0, 150], [20, 40, 20]);
const width = useSpring(widthTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const height = useSpring(heightTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const widthIcon = useSpring(widthTransformIcon, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const heightIcon = useSpring(heightTransformIcon, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const [hovered, setHovered] = useState(false);
return (
<a href={href}>
<motion.div
ref={ref}
style={{ width, height }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className="relative flex aspect-square items-center justify-center rounded-full bg-gray-200 dark:bg-neutral-800"
>
<AnimatePresence>
{hovered && (
<motion.div
initial={{ opacity: 0, y: 10, x: "-50%" }}
animate={{ opacity: 1, y: 0, x: "-50%" }}
exit={{ opacity: 0, y: 2, x: "-50%" }}
className="absolute -top-8 left-1/2 w-fit rounded-md border border-gray-200 bg-gray-100 px-2 py-0.5 text-xs whitespace-pre text-neutral-700 dark:border-neutral-900 dark:bg-neutral-800 dark:text-white"
>
{title}
</motion.div>
)}
</AnimatePresence>
<motion.div
style={{ width: widthIcon, height: heightIcon }}
className="flex items-center justify-center"
>
{icon}
</motion.div>
</motion.div>
</a>
);
}
+64
View File
@@ -0,0 +1,64 @@
"use client";
import React, { useState } from "react";
import { cn } from "@/lib/utils";
export const Card = React.memo(
({
card,
index,
hovered,
setHovered,
}: {
card: any;
index: number;
hovered: number | null;
setHovered: React.Dispatch<React.SetStateAction<number | null>>;
}) => (
<div
onMouseEnter={() => setHovered(index)}
onMouseLeave={() => setHovered(null)}
className={cn(
"relative h-60 w-full overflow-hidden rounded-lg bg-gray-100 transition-all duration-300 ease-out md:h-96 dark:bg-neutral-900",
hovered !== null && hovered !== index && "scale-[0.98] blur-sm"
)}
>
<img src={card.src} alt={card.title} className="absolute inset-0 object-cover" />
<div
className={cn(
"absolute inset-0 flex items-end bg-black/50 px-4 py-8 transition-opacity duration-300",
hovered === index ? "opacity-100" : "opacity-0"
)}
>
<div className="bg-gradient-to-b from-neutral-50 to-neutral-200 bg-clip-text text-xl font-medium text-transparent md:text-2xl">
{card.title}
</div>
</div>
</div>
)
);
Card.displayName = "Card";
type Card = {
title: string;
src: string;
};
export function FocusCards({ cards }: { cards: Card[] }) {
const [hovered, setHovered] = useState<number | null>(null);
return (
<div className="mx-auto grid w-full max-w-5xl grid-cols-1 gap-10 md:grid-cols-3 md:px-8">
{cards.map((card, index) => (
<Card
key={card.title}
card={card}
index={index}
hovered={hovered}
setHovered={setHovered}
/>
))}
</div>
);
}
File diff suppressed because one or more lines are too long
+236
View File
@@ -0,0 +1,236 @@
"use client";
import { useState, useRef, useEffect, useId, useMemo, useCallback, type ChangeEvent } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
function GooeyFilter({ filterId, blur }: { filterId: string; blur: number }) {
return (
<svg className="absolute hidden h-0 w-0" aria-hidden>
<defs>
<filter id={filterId} x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation={blur} result="blur" />
<feColorMatrix
in="blur"
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -10"
result="goo"
/>
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
</filter>
</defs>
</svg>
);
}
function SearchIcon({ layoutId }: { layoutId: string }) {
return (
<motion.svg
layoutId={layoutId}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
className="size-4 shrink-0"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</motion.svg>
);
}
const transition = {
duration: 0.4,
type: "spring" as const,
bounce: 0.25,
};
const iconBubbleVariants = {
collapsed: { scale: 0, opacity: 0 },
expanded: { scale: 1, opacity: 1 },
};
export interface GooeyInputClassNames {
root?: string;
filterWrap?: string;
buttonRow?: string;
trigger?: string;
input?: string;
bubble?: string;
bubbleSurface?: string;
}
export interface GooeyInputProps {
placeholder?: string;
className?: string;
classNames?: GooeyInputClassNames;
/** Collapsed control width in px */
collapsedWidth?: number;
/** Expanded control width in px */
expandedWidth?: number;
/** Horizontal offset when expanded (px), aligns detached bubble */
expandedOffset?: number;
/** Gaussian blur amount for the gooey SVG filter */
gooeyBlur?: number;
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
onOpenChange?: (open: boolean) => void;
disabled?: boolean;
}
export function GooeyInput({
placeholder = "Type to search...",
className,
classNames,
collapsedWidth = 115,
expandedWidth = 200,
expandedOffset = 50,
gooeyBlur = 5,
value: valueProp,
defaultValue = "",
onValueChange,
onOpenChange,
disabled = false,
}: GooeyInputProps) {
const reactId = useId();
const safeId = reactId.replace(/:/g, "");
const filterId = `gooey-filter-${safeId}`;
const iconLayoutId = `gooey-input-icon-${safeId}`;
const inputLayoutId = `gooey-input-field-${safeId}`;
const inputRef = useRef<HTMLInputElement>(null);
const prevExpandedRef = useRef(false);
const [isExpanded, setIsExpanded] = useState(false);
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
const isControlled = valueProp !== undefined;
const searchText = isControlled ? valueProp : uncontrolledValue;
const setSearchText = useCallback(
(next: string) => {
if (!isControlled) {
setUncontrolledValue(next);
}
onValueChange?.(next);
},
[isControlled, onValueChange]
);
const setExpanded = useCallback(
(next: boolean) => {
setIsExpanded(next);
onOpenChange?.(next);
},
[onOpenChange]
);
useEffect(() => {
if (isExpanded) {
inputRef.current?.focus();
} else if (prevExpandedRef.current) {
setSearchText("");
}
prevExpandedRef.current = isExpanded;
}, [isExpanded, setSearchText]);
const buttonVariants = useMemo(
() => ({
collapsed: { width: collapsedWidth, marginLeft: 0 },
expanded: { width: expandedWidth, marginLeft: expandedOffset },
}),
[collapsedWidth, expandedWidth, expandedOffset]
);
const handleExpand = useCallback(() => {
if (!disabled) setExpanded(true);
}, [disabled, setExpanded]);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setSearchText(e.target.value);
},
[setSearchText]
);
const handleBlur = useCallback(() => {
if (!searchText) setExpanded(false);
}, [searchText, setExpanded]);
const surfaceClass = "bg-foreground text-background shadow-sm ring-1 ring-border/60";
return (
<div className={cn("relative flex items-center justify-center", className, classNames?.root)}>
<GooeyFilter filterId={filterId} blur={gooeyBlur} />
<div
className={cn("relative flex h-10 items-center justify-center", classNames?.filterWrap)}
style={{ filter: `url(#${filterId})` }}
>
<motion.div
className={cn("flex h-10 items-center justify-center", classNames?.buttonRow)}
variants={buttonVariants}
initial="collapsed"
animate={isExpanded ? "expanded" : "collapsed"}
transition={transition}
>
<button
type="button"
disabled={disabled}
onClick={handleExpand}
className={cn(
"flex h-10 w-full cursor-pointer items-center justify-center gap-2 rounded-full px-4 text-sm font-medium transition-[color,box-shadow] outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50",
surfaceClass,
classNames?.trigger
)}
>
{!isExpanded ? <SearchIcon layoutId={iconLayoutId} /> : null}
<motion.input
layoutId={inputLayoutId}
ref={inputRef}
type="search"
enterKeyHint="search"
autoComplete="off"
value={searchText}
onChange={handleChange}
onBlur={handleBlur}
disabled={disabled || !isExpanded}
placeholder={placeholder}
className={cn(
"h-full min-w-0 flex-1 bg-transparent text-sm text-background outline-none",
isExpanded
? "placeholder:text-background/50 dark:placeholder:text-background/45"
: "pointer-events-none placeholder:text-background/80 dark:placeholder:text-background/70",
classNames?.input
)}
/>
</button>
</motion.div>
<motion.div
className={cn(
"absolute top-1/2 left-0 flex size-10 -translate-y-1/2 items-center justify-center",
classNames?.bubble
)}
variants={iconBubbleVariants}
initial="collapsed"
animate={isExpanded ? "expanded" : "collapsed"}
transition={transition}
>
<div
className={cn(
"flex size-10 items-center justify-center rounded-full",
surfaceClass,
classNames?.bubbleSurface
)}
>
<SearchIcon layoutId={iconLayoutId} />
</div>
</motion.div>
</div>
</div>
);
}
File diff suppressed because one or more lines are too long
+136
View File
@@ -0,0 +1,136 @@
"use client";
import { cn } from "@/lib/utils";
import { useMotionValue, motion, useMotionTemplate } from "motion/react";
import React from "react";
export const HeroHighlight = ({
children,
className,
containerClassName,
}: {
children: React.ReactNode;
className?: string;
containerClassName?: string;
}) => {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
// SVG patterns for different states and themes
const dotPatterns = {
light: {
default: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='16' height='16' fill='none'%3E%3Ccircle fill='%23d4d4d4' id='pattern-circle' cx='10' cy='10' r='2.5'%3E%3C/circle%3E%3C/svg%3E")`,
hover: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='16' height='16' fill='none'%3E%3Ccircle fill='%236366f1' id='pattern-circle' cx='10' cy='10' r='2.5'%3E%3C/circle%3E%3C/svg%3E")`,
},
dark: {
default: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='16' height='16' fill='none'%3E%3Ccircle fill='%23404040' id='pattern-circle' cx='10' cy='10' r='2.5'%3E%3C/circle%3E%3C/svg%3E")`,
hover: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='16' height='16' fill='none'%3E%3Ccircle fill='%238183f4' id='pattern-circle' cx='10' cy='10' r='2.5'%3E%3C/circle%3E%3C/svg%3E")`,
},
};
function handleMouseMove({ currentTarget, clientX, clientY }: React.MouseEvent<HTMLDivElement>) {
if (!currentTarget) return;
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top);
}
return (
<div
className={cn(
"group relative flex h-[40rem] w-full items-center justify-center bg-white dark:bg-black",
containerClassName
)}
onMouseMove={handleMouseMove}
>
<div
className="pointer-events-none absolute inset-0 dark:hidden"
style={{
backgroundImage: dotPatterns.light.default,
}}
/>
<div
className="pointer-events-none absolute inset-0 hidden dark:block"
style={{
backgroundImage: dotPatterns.dark.default,
}}
/>
<motion.div
className="pointer-events-none absolute inset-0 opacity-0 transition duration-300 group-hover:opacity-100 dark:hidden"
style={{
backgroundImage: dotPatterns.light.hover,
WebkitMaskImage: useMotionTemplate`
radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
black 0%,
transparent 100%
)
`,
maskImage: useMotionTemplate`
radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
black 0%,
transparent 100%
)
`,
}}
/>
<motion.div
className="pointer-events-none absolute inset-0 hidden opacity-0 transition duration-300 group-hover:opacity-100 dark:block"
style={{
backgroundImage: dotPatterns.dark.hover,
WebkitMaskImage: useMotionTemplate`
radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
black 0%,
transparent 100%
)
`,
maskImage: useMotionTemplate`
radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
black 0%,
transparent 100%
)
`,
}}
/>
<div className={cn("relative z-20", className)}>{children}</div>
</div>
);
};
export const Highlight = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<motion.span
initial={{
backgroundSize: "0% 100%",
}}
animate={{
backgroundSize: "100% 100%",
}}
transition={{
duration: 2,
ease: "linear",
delay: 0.5,
}}
style={{
backgroundRepeat: "no-repeat",
backgroundPosition: "left center",
display: "inline",
}}
className={cn(
`relative inline-block rounded-lg bg-gradient-to-r from-indigo-300 to-purple-300 px-1 pb-1 dark:from-indigo-500 dark:to-purple-500`,
className
)}
>
{children}
</motion.span>
);
};
+120
View File
@@ -0,0 +1,120 @@
"use client";
import React from "react";
import { motion, useScroll, useTransform, useSpring, MotionValue } from "motion/react";
export const HeroParallax = ({
products,
}: {
products: {
title: string;
link: string;
thumbnail: string;
}[];
}) => {
const firstRow = products.slice(0, 5);
const secondRow = products.slice(5, 10);
const thirdRow = products.slice(10, 15);
const ref = React.useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const springConfig = { stiffness: 300, damping: 30, bounce: 100 };
const translateX = useSpring(useTransform(scrollYProgress, [0, 1], [0, 1000]), springConfig);
const translateXReverse = useSpring(
useTransform(scrollYProgress, [0, 1], [0, -1000]),
springConfig
);
const rotateX = useSpring(useTransform(scrollYProgress, [0, 0.2], [15, 0]), springConfig);
const opacity = useSpring(useTransform(scrollYProgress, [0, 0.2], [0.2, 1]), springConfig);
const rotateZ = useSpring(useTransform(scrollYProgress, [0, 0.2], [20, 0]), springConfig);
const translateY = useSpring(useTransform(scrollYProgress, [0, 0.2], [-700, 500]), springConfig);
return (
<div
ref={ref}
className="relative flex h-[300vh] flex-col self-auto overflow-hidden py-40 antialiased [perspective:1000px] [transform-style:preserve-3d]"
>
<Header />
<motion.div
style={{
rotateX,
rotateZ,
translateY,
opacity,
}}
className=""
>
<motion.div className="mb-20 flex flex-row-reverse space-x-20 space-x-reverse">
{firstRow.map((product) => (
<ProductCard product={product} translate={translateX} key={product.title} />
))}
</motion.div>
<motion.div className="mb-20 flex flex-row space-x-20">
{secondRow.map((product) => (
<ProductCard product={product} translate={translateXReverse} key={product.title} />
))}
</motion.div>
<motion.div className="flex flex-row-reverse space-x-20 space-x-reverse">
{thirdRow.map((product) => (
<ProductCard product={product} translate={translateX} key={product.title} />
))}
</motion.div>
</motion.div>
</div>
);
};
export const Header = () => {
return (
<div className="relative top-0 left-0 mx-auto w-full max-w-7xl px-4 py-20 md:py-40">
<h1 className="text-2xl font-bold md:text-7xl dark:text-white">
The Ultimate <br /> development studio
</h1>
<p className="mt-8 max-w-2xl text-base md:text-xl dark:text-neutral-200">
We build beautiful products with the latest technologies and frameworks. We are a team of
passionate developers and designers that love to build amazing products.
</p>
</div>
);
};
export const ProductCard = ({
product,
translate,
}: {
product: {
title: string;
link: string;
thumbnail: string;
};
translate: MotionValue<number>;
}) => {
return (
<motion.div
style={{
x: translate,
}}
whileHover={{
y: -20,
}}
key={product.title}
className="group/product relative h-96 w-[30rem] shrink-0"
>
<a href={product.link} className="block group-hover/product:shadow-2xl">
<img
src={product.thumbnail}
height="600"
width="600"
className="absolute inset-0 h-full w-full object-cover object-left-top"
alt={product.title}
/>
</a>
<div className="pointer-events-none absolute inset-0 h-full w-full bg-black opacity-0 group-hover/product:opacity-80"></div>
<h2 className="absolute bottom-4 left-4 text-white opacity-0 group-hover/product:opacity-100">
{product.title}
</h2>
</motion.div>
);
};
+115
View File
@@ -0,0 +1,115 @@
"use client";
import { cn } from "@/lib/utils";
import React, { useEffect, useState } from "react";
export const InfiniteMovingCards = ({
items,
direction = "left",
speed = "fast",
pauseOnHover = true,
className,
}: {
items: {
quote: string;
name: string;
title: string;
}[];
direction?: "left" | "right";
speed?: "fast" | "normal" | "slow";
pauseOnHover?: boolean;
className?: string;
}) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const scrollerRef = React.useRef<HTMLUListElement>(null);
useEffect(() => {
addAnimation();
}, []);
const [start, setStart] = useState(false);
function addAnimation() {
if (containerRef.current && scrollerRef.current) {
const scrollerContent = Array.from(scrollerRef.current.children);
scrollerContent.forEach((item) => {
const duplicatedItem = item.cloneNode(true);
if (scrollerRef.current) {
scrollerRef.current.appendChild(duplicatedItem);
}
});
getDirection();
getSpeed();
setStart(true);
}
}
useEffect(() => {
setStart(false);
}, []);
const getDirection = () => {
if (containerRef.current) {
if (direction === "left") {
containerRef.current.style.setProperty("--animation-direction", "forwards");
} else {
containerRef.current.style.setProperty("--animation-direction", "reverse");
}
}
};
const getSpeed = () => {
if (containerRef.current) {
if (speed === "fast") {
containerRef.current.style.setProperty("--animation-duration", "20s");
} else if (speed === "normal") {
containerRef.current.style.setProperty("--animation-duration", "40s");
} else {
containerRef.current.style.setProperty("--animation-duration", "80s");
}
}
};
return (
<div
ref={containerRef}
className={cn(
"scroller relative z-20 max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]",
className
)}
>
<ul
ref={scrollerRef}
className={cn(
"flex w-max min-w-full shrink-0 flex-nowrap gap-4 py-4",
start && "animate-scroll",
pauseOnHover && "hover:[animation-play-state:paused]"
)}
>
{items.map((item) => (
<li
className="relative w-[350px] max-w-full shrink-0 rounded-2xl border border-b-0 border-zinc-200 bg-[linear-gradient(180deg,#fafafa,#f5f5f5)] px-8 py-6 md:w-[450px] dark:border-zinc-700 dark:bg-[linear-gradient(180deg,#27272a,#18181b)]"
key={item.name}
>
<blockquote>
<div
aria-hidden="true"
className="user-select-none pointer-events-none absolute -top-0.5 -left-0.5 -z-1 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]"
></div>
<span className="relative z-20 text-sm leading-[1.6] font-normal text-neutral-800 dark:text-gray-100">
{item.quote}
</span>
<div className="relative z-20 mt-6 flex flex-row items-center">
<span className="flex flex-col gap-1">
<span className="text-sm leading-[1.6] font-normal text-neutral-500 dark:text-gray-400">
{item.name}
</span>
<span className="text-sm leading-[1.6] font-normal text-neutral-500 dark:text-gray-400">
{item.title}
</span>
</span>
</div>
</blockquote>
</li>
))}
</ul>
</div>
);
};
+63
View File
@@ -0,0 +1,63 @@
"use client";
import React, { useRef, useState } from "react";
import { motion } from "motion/react";
export const MagneticButton = ({
children,
strength = 0.8,
maxDistance = 100,
}: {
children: React.ReactNode;
strength?: number;
maxDistance?: number;
}) => {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!ref.current) return;
const { width, height, left, top } = ref.current.getBoundingClientRect();
const { clientX, clientY } = e;
let x = (clientX - (left + width / 2)) * strength;
let y = (clientY - (top + height / 2)) * strength;
const distance = Math.hypot(x, y);
if (distance > maxDistance) {
const scale = maxDistance / distance;
x *= scale;
y *= scale;
}
setPosition({ x, y });
};
const handleMouseLeave = () => {
setPosition({ x: 0, y: 0 });
};
const hasMoved = position.x !== 0 || position.y !== 0;
return (
<div
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="cursor-pointer rounded-lg border border-dashed transition-colors duration-150 [--show-color:var(--color-blue-500)]"
style={{
borderColor: hasMoved ? "var(--show-color)" : "transparent",
backgroundColor: hasMoved
? "color-mix(in srgb,var(--show-color) 20%, transparent)"
: "transparent",
}}
>
<motion.div
ref={ref}
animate={{ x: position.x, y: position.y }}
transition={{ type: "spring", stiffness: 150, damping: 25, mass: 0.1 }}
>
{children}
</motion.div>
</div>
);
};
+33
View File
@@ -0,0 +1,33 @@
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
export const Meteors = ({ number, className }: { number?: number; className?: string }) => {
const meteors = new Array(number || 20).fill(true);
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }}>
{meteors.map((_, idx) => {
const meteorCount = number || 20;
// Calculate position to evenly distribute meteors across container width
const position = idx * (800 / meteorCount) - 400; // Spread across 800px range, centered
return (
<span
key={"meteor" + idx}
className={cn(
"animate-meteor-effect absolute h-0.5 w-0.5 rotate-[45deg] rounded-[9999px] bg-slate-500 shadow-[0_0_0_1px_#ffffff10]",
"before:absolute before:top-1/2 before:h-[1px] before:w-[50px] before:-translate-y-[50%] before:transform before:bg-gradient-to-r before:from-[#64748b] before:to-transparent before:content-['']",
className
)}
style={{
top: "-40px", // Start above the container
left: position + "px",
animationDelay: Math.random() * 5 + "s", // Random delay between 0-5s
animationDuration: Math.floor(Math.random() * (10 - 5) + 5) + "s", // Keep some randomness in duration
}}
></span>
);
})}
</motion.div>
);
};
+123
View File
@@ -0,0 +1,123 @@
"use client";
import React from "react";
import {
motion,
useAnimationFrame,
useMotionTemplate,
useMotionValue,
useTransform,
} from "motion/react";
import { useRef } from "react";
import { cn } from "@/lib/utils";
export function Button({
borderRadius = "1.75rem",
children,
as: Component = "button",
containerClassName,
borderClassName,
duration,
className,
...otherProps
}: {
borderRadius?: string;
children: React.ReactNode;
as?: any;
containerClassName?: string;
borderClassName?: string;
duration?: number;
className?: string;
[key: string]: any;
}) {
return (
<Component
className={cn(
"relative h-16 w-40 overflow-hidden bg-transparent p-[1px] text-xl",
containerClassName
)}
style={{
borderRadius: borderRadius,
}}
{...otherProps}
>
<div className="absolute inset-0" style={{ borderRadius: `calc(${borderRadius} * 0.96)` }}>
<MovingBorder duration={duration} rx="30%" ry="30%">
<div
className={cn(
"h-20 w-20 bg-[radial-gradient(#0ea5e9_40%,transparent_60%)] opacity-[0.8]",
borderClassName
)}
/>
</MovingBorder>
</div>
<div
className={cn(
"relative flex h-full w-full items-center justify-center border border-slate-800 bg-slate-900/[0.8] text-sm text-white antialiased backdrop-blur-xl",
className
)}
style={{
borderRadius: `calc(${borderRadius} * 0.96)`,
}}
>
{children}
</div>
</Component>
);
}
export const MovingBorder = ({
children,
duration = 3000,
rx,
ry,
...otherProps
}: {
children: React.ReactNode;
duration?: number;
rx?: string;
ry?: string;
[key: string]: any;
}) => {
const pathRef = useRef<SVGRectElement>(null);
const progress = useMotionValue<number>(0);
useAnimationFrame((time) => {
const length = pathRef.current?.getTotalLength();
if (length) {
const pxPerMillisecond = length / duration;
progress.set((time * pxPerMillisecond) % length);
}
});
const x = useTransform(progress, (val) => pathRef.current?.getPointAtLength(val).x);
const y = useTransform(progress, (val) => pathRef.current?.getPointAtLength(val).y);
const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`;
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
className="absolute h-full w-full"
width="100%"
height="100%"
{...otherProps}
>
<rect fill="none" width="100%" height="100%" rx={rx} ry={ry} ref={pathRef} />
</svg>
<motion.div
style={{
position: "absolute",
top: 0,
left: 0,
display: "inline-block",
transform,
}}
>
{children}
</motion.div>
</>
);
};
+143
View File
@@ -0,0 +1,143 @@
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import { useState, useEffect } from "react";
const CheckIcon = ({ className }: { className?: string }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={cn("h-6 w-6", className)}
>
<path d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
);
};
const CheckFilled = ({ className }: { className?: string }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className={cn("h-6 w-6", className)}
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clipRule="evenodd"
/>
</svg>
);
};
type LoadingState = {
text: string;
};
const LoaderCore = ({
loadingStates,
value = 0,
}: {
loadingStates: LoadingState[];
value?: number;
}) => {
return (
<div className="relative mx-auto mt-40 flex max-w-xl flex-col justify-start">
{loadingStates.map((loadingState, index) => {
const distance = Math.abs(index - value);
const opacity = Math.max(1 - distance * 0.2, 0); // Minimum opacity is 0, keep it 0.2 if you're sane.
return (
<motion.div
key={index}
className={cn("mb-4 flex gap-2 text-left")}
initial={{ opacity: 0, y: -(value * 40) }}
animate={{ opacity: opacity, y: -(value * 40) }}
transition={{ duration: 0.5 }}
>
<div>
{index > value && <CheckIcon className="text-black dark:text-white" />}
{index <= value && (
<CheckFilled
className={cn(
"text-black dark:text-white",
value === index && "text-black opacity-100 dark:text-lime-500"
)}
/>
)}
</div>
<span
className={cn(
"text-black dark:text-white",
value === index && "text-black opacity-100 dark:text-lime-500"
)}
>
{loadingState.text}
</span>
</motion.div>
);
})}
</div>
);
};
export const MultiStepLoader = ({
loadingStates,
loading,
duration = 2000,
loop = true,
}: {
loadingStates: LoadingState[];
loading?: boolean;
duration?: number;
loop?: boolean;
}) => {
const [currentState, setCurrentState] = useState(0);
useEffect(() => {
if (!loading) {
setCurrentState(0);
return;
}
const timeout = setTimeout(() => {
setCurrentState((prevState) =>
loop
? prevState === loadingStates.length - 1
? 0
: prevState + 1
: Math.min(prevState + 1, loadingStates.length - 1)
);
}, duration);
return () => clearTimeout(timeout);
}, [currentState, loading, loop, loadingStates.length, duration]);
return (
<AnimatePresence mode="wait">
{loading && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
}}
className="fixed inset-0 z-[100] flex h-full w-full items-center justify-center backdrop-blur-2xl"
>
<div className="relative h-96">
<LoaderCore value={currentState} loadingStates={loadingStates} />
</div>
<div className="absolute inset-x-0 bottom-0 z-20 h-full bg-white bg-gradient-to-t [mask-image:radial-gradient(900px_at_center,transparent_30%,white)] dark:bg-black" />
</motion.div>
)}
</AnimatePresence>
);
};
+290
View File
@@ -0,0 +1,290 @@
"use client";
import React, { useEffect, useId, useRef, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/lib/utils";
export type NotchOption = {
/** Stable identifier passed back in callbacks. */
id: string;
/** What renders for this option. Can be text or any node. */
label: React.ReactNode;
/** Optional leading node (icon, swatch, etc.) shown before the label. */
icon?: React.ReactNode;
};
export type NotchItem = {
/** Stable identifier for the group. */
id: string;
/** Trigger label shown in the bar. */
label: React.ReactNode;
/** Optional leading icon for the trigger. */
icon?: React.ReactNode;
/** The choices revealed when the group is opened. */
options: NotchOption[];
/** Uncontrolled initial selected option id. */
defaultValue?: string;
/** Controlled selected option id. */
value?: string;
/** Show the selected value next to the trigger label. Overrides `showSelectedValue`. */
showValue?: boolean;
/** Fires with the selected option whenever it changes. */
onChange?: (optionId: string, option: NotchOption) => void;
};
export interface NotchProps {
/** The groups shown inside the notch. Pass one or many. */
items: NotchItem[];
/** Pin the notch to the top or bottom of the viewport. */
position?: "top" | "bottom";
/** Horizontal alignment of the floating notch. */
align?: "start" | "center" | "end";
/** Fired for any group change, in addition to the per-item callback. */
onItemChange?: (itemId: string, optionId: string, option: NotchOption) => void;
/** Close the panel after selecting an option. */
closeOnSelect?: boolean;
/** Show each group's selected value next to its trigger label. */
showSelectedValue?: boolean;
/** Render dotted dividers between groups. */
showDividers?: boolean;
/** Highlight color for the selected option. Any CSS color or variable. */
accentColor?: string;
/** Distance from the pinned edge, in pixels. */
offset?: number;
/** Play the entrance animation on mount. */
reveal?: boolean;
/** Classes applied to the floating shell. */
className?: string;
/** Classes applied to every trigger. */
itemClassName?: string;
/** Classes applied to the options panel. */
panelClassName?: string;
}
const SHELL_SPRING = { type: "spring" as const, stiffness: 380, damping: 34 };
const LIST_VARIANTS = {
hidden: {},
visible: { transition: { staggerChildren: 0.045, delayChildren: 0.08 } },
};
const OPTION_VARIANTS = {
hidden: { opacity: 0, y: -10, filter: "blur(4px)" },
visible: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { type: "spring" as const, stiffness: 420, damping: 30 },
},
};
function NotchDivider() {
return (
<span
aria-hidden
className="mx-0.5 h-5 w-px shrink-0 self-center"
style={{
backgroundImage:
"repeating-linear-gradient(180deg, rgba(255,255,255,0.35) 0px, rgba(255,255,255,0.35) 1px, transparent 1px, transparent 5px)",
backgroundSize: "1px 4px",
backgroundRepeat: "repeat-y",
}}
/>
);
}
export const Notch = ({
items,
position = "bottom",
align = "center",
onItemChange,
closeOnSelect = true,
showSelectedValue = true,
showDividers = true,
accentColor = "var(--color-blue-500, #3b82f6)",
offset = 16,
reveal = true,
className,
itemClassName,
panelClassName,
}: NotchProps) => {
const shellRef = useRef<HTMLDivElement>(null);
const shellLayoutId = useId();
const [openItemId, setOpenItemId] = useState<string | null>(null);
const [internalSelected, setInternalSelected] = useState<Record<string, string>>(() => {
const map: Record<string, string> = {};
for (const item of items) {
if (item.value === undefined) {
map[item.id] = item.defaultValue ?? item.options[0]?.id ?? "";
}
}
return map;
});
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") setOpenItemId(null);
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
useEffect(() => {
if (!openItemId) return;
function onPointerDown(e: PointerEvent) {
if (!shellRef.current?.contains(e.target as Node)) setOpenItemId(null);
}
document.addEventListener("pointerdown", onPointerDown);
return () => document.removeEventListener("pointerdown", onPointerDown);
}, [openItemId]);
const getSelectedId = (item: NotchItem) =>
item.value ?? internalSelected[item.id] ?? item.options[0]?.id;
const getSelectedOption = (item: NotchItem) =>
item.options.find((o) => o.id === getSelectedId(item));
const handleSelect = (item: NotchItem, option: NotchOption) => {
if (item.value === undefined) {
setInternalSelected((prev) => ({ ...prev, [item.id]: option.id }));
}
item.onChange?.(option.id, option);
onItemChange?.(item.id, option.id, option);
if (closeOnSelect) setOpenItemId(null);
};
const alignClass =
align === "start" ? "justify-start" : align === "end" ? "justify-end" : "justify-center";
const edgeOffset = (offset + 20) * (position === "top" ? -1 : 1);
const openItem = items.find((i) => i.id === openItemId) ?? null;
const optionsPanel = openItem ? (
<motion.div
key={openItem.id}
role="listbox"
aria-label={typeof openItem.label === "string" ? openItem.label : openItem.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className={cn("w-fit", panelClassName)}
>
<motion.div
className="flex flex-col gap-1.5 p-2"
variants={LIST_VARIANTS}
initial="hidden"
animate="visible"
>
{openItem.options.map((option) => {
const active = option.id === getSelectedId(openItem);
return (
<motion.button
key={option.id}
role="option"
aria-selected={active}
type="button"
variants={OPTION_VARIANTS}
onClick={() => handleSelect(openItem, option)}
className={cn(
"flex w-full items-center justify-between gap-6 rounded-md px-3 py-2 text-left text-xs font-medium whitespace-nowrap transition-colors",
active ? "text-white" : "text-neutral-300 hover:bg-white/5 hover:text-white"
)}
style={
active
? {
background: `color-mix(in oklab, ${accentColor} 85%, transparent)`,
boxShadow: `inset 0 0 0 1px color-mix(in oklab, ${accentColor} 40%, transparent)`,
}
: undefined
}
>
<span className="flex items-center gap-2.5">
{option.icon ? (
<span className="flex shrink-0 items-center justify-center">{option.icon}</span>
) : null}
<span>{option.label}</span>
</span>
{active ? (
<span
className="size-1.5 shrink-0 rounded-full"
style={{ background: accentColor }}
/>
) : null}
</motion.button>
);
})}
</motion.div>
</motion.div>
) : (
<motion.div
key="__notch-triggers"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="flex w-fit items-center gap-1 p-1"
>
{items.map((item, index) => {
const selected = getSelectedOption(item);
const isLast = index === items.length - 1;
return (
<React.Fragment key={item.id}>
<button
type="button"
aria-haspopup="listbox"
aria-expanded={false}
onClick={() => setOpenItemId(item.id)}
className={cn(
"group flex items-center gap-2 rounded-full px-3 py-2 text-xs font-medium whitespace-nowrap text-neutral-300 transition-colors hover:bg-white/6 hover:text-white",
itemClassName
)}
>
{item.icon ? (
<span className="flex shrink-0 items-center justify-center">{item.icon}</span>
) : null}
<span className="text-neutral-100">{item.label}</span>
{(item.showValue ?? showSelectedValue) && selected ? (
<span className="text-neutral-400">{selected.label}</span>
) : null}
</button>
{showDividers && !isLast ? <NotchDivider /> : null}
</React.Fragment>
);
})}
</motion.div>
);
return (
<div
className={cn(
"pointer-events-none fixed inset-x-0 z-100 flex translate-z-0 px-4",
position === "top" ? "top-0" : "bottom-0",
alignClass
)}
style={
position === "top"
? { paddingTop: `max(${offset}px, env(safe-area-inset-top))` }
: { paddingBottom: `max(${offset}px, env(safe-area-inset-bottom))` }
}
>
<motion.div
ref={shellRef}
layoutId={shellLayoutId}
layout
initial={reveal ? { opacity: 0, y: edgeOffset, filter: "blur(6px)" } : false}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={SHELL_SPRING}
className={cn(
"pointer-events-auto flex w-fit flex-col overflow-hidden rounded-xl border border-white/10 bg-neutral-950/95 shadow-[0_12px_40px_-8px_rgba(0,0,0,0.55)] ring-1 ring-neutral-800 backdrop-blur-2xl ring-inset",
className
)}
>
<AnimatePresence mode="popLayout" initial={false}>
{optionsPanel}
</AnimatePresence>
</motion.div>
</div>
);
};
+76
View File
@@ -0,0 +1,76 @@
"use client";
import { useScroll, useTransform } from "motion/react";
import { useRef } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const ParallaxScroll = ({ images, className }: { images: string[]; className?: string }) => {
const gridRef = useRef<any>(null);
const { scrollYProgress } = useScroll({
container: gridRef, // remove this if your container is not fixed height
offset: ["start start", "end start"], // remove this if your container is not fixed height
});
const translateFirst = useTransform(scrollYProgress, [0, 1], [0, -200]);
const translateSecond = useTransform(scrollYProgress, [0, 1], [0, 200]);
const translateThird = useTransform(scrollYProgress, [0, 1], [0, -200]);
const third = Math.ceil(images.length / 3);
const firstPart = images.slice(0, third);
const secondPart = images.slice(third, 2 * third);
const thirdPart = images.slice(2 * third);
return (
<div className={cn("h-[40rem] w-full items-start overflow-y-auto", className)} ref={gridRef}>
<div
className="mx-auto grid max-w-5xl grid-cols-1 items-start gap-10 px-10 py-40 md:grid-cols-2 lg:grid-cols-3"
ref={gridRef}
>
<div className="grid gap-10">
{firstPart.map((el, idx) => (
<motion.div
style={{ y: translateFirst }} // Apply the translateY motion value here
key={"grid-1" + idx}
>
<img
src={el}
className="!m-0 h-80 w-full gap-10 rounded-lg object-cover object-left-top !p-0"
height="400"
width="400"
alt="thumbnail"
/>
</motion.div>
))}
</div>
<div className="grid gap-10">
{secondPart.map((el, idx) => (
<motion.div style={{ y: translateSecond }} key={"grid-2" + idx}>
<img
src={el}
className="!m-0 h-80 w-full gap-10 rounded-lg object-cover object-left-top !p-0"
height="400"
width="400"
alt="thumbnail"
/>
</motion.div>
))}
</div>
<div className="grid gap-10">
{thirdPart.map((el, idx) => (
<motion.div style={{ y: translateThird }} key={"grid-3" + idx}>
<img
src={el}
className="!m-0 h-80 w-full gap-10 rounded-lg object-cover object-left-top !p-0"
height="400"
width="400"
alt="thumbnail"
/>
</motion.div>
))}
</div>
</div>
</div>
);
};
@@ -0,0 +1,267 @@
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
export function PlaceholdersAndVanishInput({
placeholders,
onChange,
onSubmit,
}: {
placeholders: string[];
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
}) {
const [currentPlaceholder, setCurrentPlaceholder] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const startAnimation = () => {
intervalRef.current = setInterval(() => {
setCurrentPlaceholder((prev) => (prev + 1) % placeholders.length);
}, 3000);
};
const handleVisibilityChange = () => {
if (document.visibilityState !== "visible" && intervalRef.current) {
clearInterval(intervalRef.current); // Clear the interval when the tab is not visible
intervalRef.current = null;
} else if (document.visibilityState === "visible") {
startAnimation(); // Restart the interval when the tab becomes visible
}
};
useEffect(() => {
startAnimation();
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [placeholders]);
const canvasRef = useRef<HTMLCanvasElement>(null);
const newDataRef = useRef<any[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState("");
const [animating, setAnimating] = useState(false);
const draw = useCallback(() => {
if (!inputRef.current) return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
canvas.width = 800;
canvas.height = 800;
ctx.clearRect(0, 0, 800, 800);
const computedStyles = getComputedStyle(inputRef.current);
const fontSize = parseFloat(computedStyles.getPropertyValue("font-size"));
ctx.font = `${fontSize * 2}px ${computedStyles.fontFamily}`;
ctx.fillStyle = "#FFF";
ctx.fillText(value, 16, 40);
const imageData = ctx.getImageData(0, 0, 800, 800);
const pixelData = imageData.data;
const newData: any[] = [];
for (let t = 0; t < 800; t++) {
const i = 4 * t * 800;
for (let n = 0; n < 800; n++) {
const e = i + 4 * n;
if (pixelData[e] !== 0 && pixelData[e + 1] !== 0 && pixelData[e + 2] !== 0) {
newData.push({
x: n,
y: t,
color: [pixelData[e], pixelData[e + 1], pixelData[e + 2], pixelData[e + 3]],
});
}
}
}
newDataRef.current = newData.map(({ x, y, color }) => ({
x,
y,
r: 1,
color: `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`,
}));
}, [value]);
useEffect(() => {
draw();
}, [value, draw]);
const animate = (start: number) => {
const animateFrame = (pos: number = 0) => {
requestAnimationFrame(() => {
const newArr = [];
for (let i = 0; i < newDataRef.current.length; i++) {
const current = newDataRef.current[i];
if (current.x < pos) {
newArr.push(current);
} else {
if (current.r <= 0) {
current.r = 0;
continue;
}
current.x += Math.random() > 0.5 ? 1 : -1;
current.y += Math.random() > 0.5 ? 1 : -1;
current.r -= 0.05 * Math.random();
newArr.push(current);
}
}
newDataRef.current = newArr;
const ctx = canvasRef.current?.getContext("2d");
if (ctx) {
ctx.clearRect(pos, 0, 800, 800);
newDataRef.current.forEach((t) => {
const { x: n, y: i, r: s, color: color } = t;
if (n > pos) {
ctx.beginPath();
ctx.rect(n, i, s, s);
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.stroke();
}
});
}
if (newDataRef.current.length > 0) {
animateFrame(pos - 8);
} else {
setValue("");
setAnimating(false);
}
});
};
animateFrame(start);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !animating) {
vanishAndSubmit();
}
};
const vanishAndSubmit = () => {
setAnimating(true);
draw();
const value = inputRef.current?.value || "";
if (value && inputRef.current) {
const maxX = newDataRef.current.reduce(
(prev, current) => (current.x > prev ? current.x : prev),
0
);
animate(maxX);
}
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
vanishAndSubmit();
onSubmit && onSubmit(e);
};
return (
<form
className={cn(
"relative mx-auto h-12 w-full max-w-xl overflow-hidden rounded-full bg-white shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),_0px_1px_0px_0px_rgba(25,28,33,0.02),_0px_0px_0px_1px_rgba(25,28,33,0.08)] transition duration-200 dark:bg-zinc-800",
value && "bg-gray-50"
)}
onSubmit={handleSubmit}
>
<canvas
className={cn(
"pointer-events-none absolute top-[20%] left-2 origin-top-left scale-50 transform pr-20 text-base invert filter sm:left-8 dark:invert-0",
!animating ? "opacity-0" : "opacity-100"
)}
ref={canvasRef}
/>
<input
onChange={(e) => {
if (!animating) {
setValue(e.target.value);
onChange && onChange(e);
}
}}
onKeyDown={handleKeyDown}
ref={inputRef}
value={value}
type="text"
className={cn(
"relative z-50 h-full w-full rounded-full border-none bg-transparent pr-20 pl-4 text-sm text-black focus:ring-0 focus:outline-none sm:pl-10 sm:text-base dark:text-white",
animating && "text-transparent dark:text-transparent"
)}
/>
<button
disabled={!value}
type="submit"
className="absolute top-1/2 right-2 z-50 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-black transition duration-200 disabled:bg-gray-100 dark:bg-zinc-900 dark:disabled:bg-zinc-800"
>
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-gray-300"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<motion.path
d="M5 12l14 0"
initial={{
strokeDasharray: "50%",
strokeDashoffset: "50%",
}}
animate={{
strokeDashoffset: value ? 0 : "50%",
}}
transition={{
duration: 0.3,
ease: "linear",
}}
/>
<path d="M13 18l6 -6" />
<path d="M13 6l6 6" />
</motion.svg>
</button>
<div className="pointer-events-none absolute inset-0 flex items-center rounded-full">
<AnimatePresence mode="wait">
{!value && (
<motion.p
initial={{
y: 5,
opacity: 0,
}}
key={`current-placeholder-${currentPlaceholder}`}
animate={{
y: 0,
opacity: 1,
}}
exit={{
y: -15,
opacity: 0,
}}
transition={{
duration: 0.3,
ease: "linear",
}}
className="w-[calc(100%-2rem)] truncate pl-4 text-left text-sm font-normal text-neutral-500 sm:pl-12 sm:text-base dark:text-zinc-500"
>
{placeholders[currentPlaceholder]}
</motion.p>
)}
</AnimatePresence>
</div>
</form>
);
}
+426
View File
@@ -0,0 +1,426 @@
"use client";
import { useId } from "react";
import { useEffect, useState } from "react";
import Particles, { initParticlesEngine } from "@tsparticles/react";
import type { Container, SingleOrMultiple } from "@tsparticles/engine";
import { loadSlim } from "@tsparticles/slim";
import { cn } from "@/lib/utils";
import { motion, useAnimation } from "motion/react";
type ParticlesProps = {
id?: string;
className?: string;
background?: string;
particleSize?: number;
minSize?: number;
maxSize?: number;
speed?: number;
particleColor?: string;
particleDensity?: number;
};
export const SparklesCore = (props: ParticlesProps) => {
const { id, className, background, minSize, maxSize, speed, particleColor, particleDensity } =
props;
const [init, setInit] = useState(false);
useEffect(() => {
initParticlesEngine(async (engine) => {
await loadSlim(engine);
}).then(() => {
setInit(true);
});
}, []);
const controls = useAnimation();
const particlesLoaded = async (container?: Container) => {
if (container) {
controls.start({
opacity: 1,
transition: {
duration: 1,
},
});
}
};
const generatedId = useId();
return (
<motion.div animate={controls} className={cn("opacity-0", className)}>
{init && (
<Particles
id={id || generatedId}
className={cn("h-full w-full")}
particlesLoaded={particlesLoaded}
options={{
background: {
color: {
value: background || "#0d47a1",
},
},
fullScreen: {
enable: false,
zIndex: 1,
},
fpsLimit: 120,
interactivity: {
events: {
onClick: {
enable: true,
mode: "push",
},
onHover: {
enable: false,
mode: "repulse",
},
resize: true as any,
},
modes: {
push: {
quantity: 4,
},
repulse: {
distance: 200,
duration: 0.4,
},
},
},
particles: {
bounce: {
horizontal: {
value: 1,
},
vertical: {
value: 1,
},
},
collisions: {
absorb: {
speed: 2,
},
bounce: {
horizontal: {
value: 1,
},
vertical: {
value: 1,
},
},
enable: false,
maxSpeed: 50,
mode: "bounce",
overlap: {
enable: true,
retries: 0,
},
},
color: {
value: particleColor || "#ffffff",
animation: {
h: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: true,
offset: 0,
},
s: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: true,
offset: 0,
},
l: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: true,
offset: 0,
},
},
},
effect: {
close: true,
fill: true,
options: {},
type: {} as SingleOrMultiple<string> | undefined,
},
groups: {},
move: {
angle: {
offset: 0,
value: 90,
},
attract: {
distance: 200,
enable: false,
rotate: {
x: 3000,
y: 3000,
},
},
center: {
x: 50,
y: 50,
mode: "percent",
radius: 0,
},
decay: 0,
distance: {},
direction: "none",
drift: 0,
enable: true,
gravity: {
acceleration: 9.81,
enable: false,
inverse: false,
maxSpeed: 50,
},
path: {
clamp: true,
delay: {
value: 0,
},
enable: false,
options: {},
},
outModes: {
default: "out",
},
random: false,
size: false,
speed: {
min: 0.1,
max: 1,
},
spin: {
acceleration: 0,
enable: false,
},
straight: false,
trail: {
enable: false,
length: 10,
fill: {},
},
vibrate: false,
warp: false,
},
number: {
density: {
enable: true,
width: 400,
height: 400,
},
limit: {
mode: "delete",
value: 0,
},
value: particleDensity || 120,
},
opacity: {
value: {
min: 0.1,
max: 1,
},
animation: {
count: 0,
enable: true,
speed: speed || 4,
decay: 0,
delay: 0,
sync: false,
mode: "auto",
startValue: "random",
destroy: "none",
},
},
reduceDuplicates: false,
shadow: {
blur: 0,
color: {
value: "#000",
},
enable: false,
offset: {
x: 0,
y: 0,
},
},
shape: {
close: true,
fill: true,
options: {},
type: "circle",
},
size: {
value: {
min: minSize || 1,
max: maxSize || 3,
},
animation: {
count: 0,
enable: false,
speed: 5,
decay: 0,
delay: 0,
sync: false,
mode: "auto",
startValue: "random",
destroy: "none",
},
},
stroke: {
width: 0,
},
zIndex: {
value: 0,
opacityRate: 1,
sizeRate: 1,
velocityRate: 1,
},
destroy: {
bounds: {},
mode: "none",
split: {
count: 1,
factor: {
value: 3,
},
rate: {
value: {
min: 4,
max: 9,
},
},
sizeOffset: true,
},
},
roll: {
darken: {
enable: false,
value: 0,
},
enable: false,
enlighten: {
enable: false,
value: 0,
},
mode: "vertical",
speed: 25,
},
tilt: {
value: 0,
animation: {
enable: false,
speed: 0,
decay: 0,
sync: false,
},
direction: "clockwise",
enable: false,
},
twinkle: {
lines: {
enable: false,
frequency: 0.05,
opacity: 1,
},
particles: {
enable: false,
frequency: 0.05,
opacity: 1,
},
},
wobble: {
distance: 5,
enable: false,
speed: {
angle: 50,
move: 10,
},
},
life: {
count: 0,
delay: {
value: 0,
sync: false,
},
duration: {
value: 0,
sync: false,
},
},
rotate: {
value: 0,
animation: {
enable: false,
speed: 0,
decay: 0,
sync: false,
},
direction: "clockwise",
path: false,
},
orbit: {
animation: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: false,
},
enable: false,
opacity: 1,
rotation: {
value: 45,
},
width: 1,
},
links: {
blink: false,
color: {
value: "#fff",
},
consent: false,
distance: 100,
enable: false,
frequency: 1,
opacity: 1,
shadow: {
blur: 5,
color: {
value: "#000",
},
enable: false,
},
triangles: {
enable: false,
frequency: 1,
},
width: 1,
warp: false,
},
repulse: {
value: 0,
enabled: false,
distance: 1,
duration: 1,
factor: 1,
speed: 1,
},
},
detectRetina: true,
}}
/>
)}
</motion.div>
);
};
+55
View File
@@ -0,0 +1,55 @@
import { cn } from "@/lib/utils";
type SpotlightProps = {
className?: string;
fill?: string;
};
export const Spotlight = ({ className, fill }: SpotlightProps) => {
return (
<svg
className={cn(
"animate-spotlight pointer-events-none absolute z-[1] h-[169%] w-[138%] opacity-0 lg:w-[84%]",
className
)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 3787 2842"
fill="none"
>
<g filter="url(#filter)">
<ellipse
cx="1924.71"
cy="273.501"
rx="1924.71"
ry="273.501"
transform="matrix(-0.822377 -0.568943 -0.568943 0.822377 3631.88 2291.09)"
fill={fill || "white"}
fillOpacity="0.21"
></ellipse>
</g>
<defs>
<filter
id="filter"
x="0.860352"
y="0.838989"
width="3785.16"
height="2840.26"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix"></feFlood>
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
></feBlend>
<feGaussianBlur
stdDeviation="151"
result="effect1_foregroundBlur_1065_8"
></feGaussianBlur>
</filter>
</defs>
</svg>
);
};
+104
View File
@@ -0,0 +1,104 @@
"use client";
import React, { useId } from "react";
import { motion, useTime, useTransform } from "motion/react";
import { cn } from "@/lib/utils";
export interface SquigglyTextProps {
/**
* The text (or any node) to wrap with the squiggly effect.
*/
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
/**
* Number of distinct displacement frames to cycle through.
* Higher = smoother wobble, more SVG filters in the DOM.
* @default 5
*/
steps?: number;
/**
* Time between filter swaps, in milliseconds.
* Lower = more frantic; higher = lazier wave.
* @default 80
*/
stepDuration?: number;
/**
* Maximum displacement in px. Bigger = more squiggly.
* Pass a single number for a constant scale, or a tuple to alternate
* between two values per step (matches the original Lucas Bebber demo).
* @default [6, 8]
*/
scale?: number | [number, number];
/**
* Turbulence base frequency. Lower values produce longer, smoother waves;
* higher values produce tighter, jitterier noise.
* @default 0.02
*/
baseFrequency?: number;
/**
* Number of turbulence octaves. Higher = more detailed noise.
* @default 3
*/
numOctaves?: number;
/**
* Render the wrapper as this element type.
* @default "span"
*/
as?: "span" | "div";
}
export function SquigglyText({
children,
steps = 5,
stepDuration = 80,
scale = [6, 8],
baseFrequency = 0.02,
numOctaves = 3,
as = "span",
className,
style,
}: SquigglyTextProps) {
const reactId = useId();
// useId can produce ":" / "_" which aren't valid in CSS url(#…) refs.
const safeId = reactId.replace(/[:_]/g, "");
const filterId = (i: number) => `squiggly-${safeId}-${i}`;
const filters = React.useMemo(
() => Array.from({ length: steps }, (_, i) => `url(#${filterId(i)})`),
// filterId is stable per instance
[steps, safeId]
);
const time = useTime();
const filter = useTransform(time, (t) => filters[Math.floor(t / stepDuration) % filters.length]);
const scaleAt = (i: number) => (Array.isArray(scale) ? scale[i % scale.length] : scale);
const Wrapper = as === "div" ? motion.div : motion.span;
return (
<Wrapper style={{ filter, ...style }} className={cn("inline-block", className)}>
<svg
aria-hidden
className="pointer-events-none absolute h-0 w-0 overflow-hidden"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{Array.from({ length: steps }).map((_, i) => (
<filter id={filterId(i)} key={i}>
<feTurbulence
baseFrequency={baseFrequency}
numOctaves={numOctaves}
result="noise"
seed={i}
/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale={scaleAt(i)} />
</filter>
))}
</defs>
</svg>
{children}
</Wrapper>
);
}
+107
View File
@@ -0,0 +1,107 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { useMotionValueEvent, useScroll } from "motion/react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const StickyScroll = ({
content,
contentClassName,
}: {
content: {
title: string;
description: string;
content?: React.ReactNode | any;
}[];
contentClassName?: string;
}) => {
const [activeCard, setActiveCard] = React.useState(0);
const ref = useRef<any>(null);
const { scrollYProgress } = useScroll({
// uncomment line 22 and comment line 23 if you DONT want the overflow container and want to have it change on the entire page scroll
// target: ref
container: ref,
offset: ["start start", "end start"],
});
const cardLength = content.length;
useMotionValueEvent(scrollYProgress, "change", (latest) => {
const cardsBreakpoints = content.map((_, index) => index / cardLength);
const closestBreakpointIndex = cardsBreakpoints.reduce((acc, breakpoint, index) => {
const distance = Math.abs(latest - breakpoint);
if (distance < Math.abs(latest - cardsBreakpoints[acc])) {
return index;
}
return acc;
}, 0);
setActiveCard(closestBreakpointIndex);
});
const backgroundColors = [
"#0f172a", // slate-900
"#000000", // black
"#171717", // neutral-900
];
const linearGradients = [
"linear-gradient(to bottom right, #06b6d4, #10b981)", // cyan-500 to emerald-500
"linear-gradient(to bottom right, #ec4899, #6366f1)", // pink-500 to indigo-500
"linear-gradient(to bottom right, #f97316, #eab308)", // orange-500 to yellow-500
];
const [backgroundGradient, setBackgroundGradient] = useState(linearGradients[0]);
useEffect(() => {
setBackgroundGradient(linearGradients[activeCard % linearGradients.length]);
}, [activeCard]);
return (
<motion.div
animate={{
backgroundColor: backgroundColors[activeCard % backgroundColors.length],
}}
className="relative flex h-[30rem] justify-center space-x-10 overflow-y-auto rounded-md p-10"
ref={ref}
>
<div className="div relative flex items-start px-4">
<div className="max-w-2xl">
{content.map((item, index) => (
<div key={item.title + index} className="my-20">
<motion.h2
initial={{
opacity: 0,
}}
animate={{
opacity: activeCard === index ? 1 : 0.3,
}}
className="text-2xl font-bold text-slate-100"
>
{item.title}
</motion.h2>
<motion.p
initial={{
opacity: 0,
}}
animate={{
opacity: activeCard === index ? 1 : 0.3,
}}
className="text-kg mt-10 max-w-sm text-slate-300"
>
{item.description}
</motion.p>
</div>
))}
<div className="h-40" />
</div>
</div>
<div
style={{ background: backgroundGradient }}
className={cn(
"sticky top-10 hidden h-60 w-80 overflow-hidden rounded-md bg-white lg:block",
contentClassName
)}
>
{content[activeCard].content ?? null}
</div>
</motion.div>
);
};
+76
View File
@@ -0,0 +1,76 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const MaskContainer = ({
children,
revealText,
size = 10,
revealSize = 600,
className,
}: {
children?: string | React.ReactNode;
revealText?: string | React.ReactNode;
size?: number;
revealSize?: number;
className?: string;
}) => {
const [isHovered, setIsHovered] = useState(false);
const [mousePosition, setMousePosition] = useState<any>({ x: null, y: null });
const containerRef = useRef<any>(null);
const updateMousePosition = (e: any) => {
const rect = containerRef.current.getBoundingClientRect();
setMousePosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
};
useEffect(() => {
containerRef.current.addEventListener("mousemove", updateMousePosition);
return () => {
if (containerRef.current) {
containerRef.current.removeEventListener("mousemove", updateMousePosition);
}
};
}, []);
const maskSize = isHovered ? revealSize : size;
return (
<motion.div
ref={containerRef}
className={cn("relative h-screen", className)}
animate={{
backgroundColor: isHovered ? "var(--slate-900)" : "var(--white)",
}}
transition={{
backgroundColor: { duration: 0.3 },
}}
>
<motion.div
className="absolute flex h-full w-full items-center justify-center bg-black [mask-image:url(/mask.svg)] [mask-size:40px] [mask-repeat:no-repeat] text-6xl dark:bg-white"
animate={{
maskPosition: `${mousePosition.x - maskSize / 2}px ${mousePosition.y - maskSize / 2}px`,
maskSize: `${maskSize}px`,
}}
transition={{
maskSize: { duration: 0.3, ease: "easeInOut" },
maskPosition: { duration: 0.15, ease: "linear" },
}}
>
<div className="absolute inset-0 z-0 h-full w-full bg-black opacity-50 dark:bg-white" />
<div
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => {
setIsHovered(false);
}}
className="relative z-20 mx-auto max-w-4xl text-center text-4xl font-bold"
>
{children}
</div>
</motion.div>
<div className="flex h-full w-full items-center justify-center">{revealText}</div>
</motion.div>
);
};
+62
View File
@@ -0,0 +1,62 @@
"use client";
import { useEffect } from "react";
import { motion, stagger, useAnimate } from "motion/react";
import { cn } from "@/lib/utils";
export const TextGenerateEffect = ({
words,
className,
filter = true,
duration = 0.5,
}: {
words: string;
className?: string;
filter?: boolean;
duration?: number;
}) => {
const [scope, animate] = useAnimate();
const wordsArray = words.split(" ");
useEffect(() => {
animate(
"span",
{
opacity: 1,
filter: filter ? "blur(0px)" : "none",
},
{
duration: duration ? duration : 1,
delay: stagger(0.2),
}
);
}, [scope.current]);
const renderWords = () => {
return (
<motion.div ref={scope}>
{wordsArray.map((word, idx) => {
return (
<motion.span
key={word + idx}
className="text-black opacity-0 dark:text-white"
style={{
filter: filter ? "blur(10px)" : "none",
}}
>
{word}{" "}
</motion.span>
);
})}
</motion.div>
);
};
return (
<div className={cn("font-bold", className)}>
<div className="mt-4">
<div className="text-2xl leading-snug tracking-wide text-black dark:text-white">
{renderWords()}
</div>
</div>
</div>
);
};
+122
View File
@@ -0,0 +1,122 @@
"use client";
import { useRef, useEffect, useState } from "react";
import { motion } from "motion/react";
export const TextHoverEffect = ({
text,
duration,
}: {
text: string;
duration?: number;
automatic?: boolean;
}) => {
const svgRef = useRef<SVGSVGElement>(null);
const [cursor, setCursor] = useState({ x: 0, y: 0 });
const [hovered, setHovered] = useState(false);
const [maskPosition, setMaskPosition] = useState({ cx: "50%", cy: "50%" });
useEffect(() => {
if (svgRef.current && cursor.x !== null && cursor.y !== null) {
const svgRect = svgRef.current.getBoundingClientRect();
const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100;
const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100;
setMaskPosition({
cx: `${cxPercentage}%`,
cy: `${cyPercentage}%`,
});
}
}, [cursor]);
return (
<svg
ref={svgRef}
width="100%"
height="100%"
viewBox="0 0 300 100"
xmlns="http://www.w3.org/2000/svg"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onMouseMove={(e) => setCursor({ x: e.clientX, y: e.clientY })}
className="select-none"
>
<defs>
<linearGradient id="textGradient" gradientUnits="userSpaceOnUse" cx="50%" cy="50%" r="25%">
{hovered && (
<>
<stop offset="0%" stopColor="#eab308" />
<stop offset="25%" stopColor="#ef4444" />
<stop offset="50%" stopColor="#3b82f6" />
<stop offset="75%" stopColor="#06b6d4" />
<stop offset="100%" stopColor="#8b5cf6" />
</>
)}
</linearGradient>
<motion.radialGradient
id="revealMask"
gradientUnits="userSpaceOnUse"
r="20%"
initial={{ cx: "50%", cy: "50%" }}
animate={maskPosition}
transition={{ duration: duration ?? 0, ease: "easeOut" }}
// example for a smoother animation below
// transition={{
// type: "spring",
// stiffness: 300,
// damping: 50,
// }}
>
<stop offset="0%" stopColor="white" />
<stop offset="100%" stopColor="black" />
</motion.radialGradient>
<mask id="textMask">
<rect x="0" y="0" width="100%" height="100%" fill="url(#revealMask)" />
</mask>
</defs>
<text
x="50%"
y="50%"
textAnchor="middle"
dominantBaseline="middle"
strokeWidth="0.3"
className="fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800"
style={{ opacity: hovered ? 0.7 : 0 }}
>
{text}
</text>
<motion.text
x="50%"
y="50%"
textAnchor="middle"
dominantBaseline="middle"
strokeWidth="0.3"
className="fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800"
initial={{ strokeDashoffset: 1000, strokeDasharray: 1000 }}
animate={{
strokeDashoffset: 0,
strokeDasharray: 1000,
}}
transition={{
duration: 4,
ease: "easeInOut",
}}
>
{text}
</motion.text>
<text
x="50%"
y="50%"
textAnchor="middle"
dominantBaseline="middle"
stroke="url(#textGradient)"
strokeWidth="0.3"
mask="url(#textMask)"
className="fill-transparent font-[helvetica] text-7xl font-bold"
>
{text}
</text>
</svg>
);
};
+180
View File
@@ -0,0 +1,180 @@
"use client";
import React, { useEffect, useRef, useState, memo } from "react";
import { motion } from "motion/react";
import { twMerge } from "tailwind-merge";
import { cn } from "@/lib/utils";
export const TextRevealCard = ({
text,
revealText,
children,
className,
}: {
text: string;
revealText: string;
children?: React.ReactNode;
className?: string;
}) => {
const [widthPercentage, setWidthPercentage] = useState(0);
const cardRef = useRef<HTMLDivElement | any>(null);
const [left, setLeft] = useState(0);
const [localWidth, setLocalWidth] = useState(0);
const [isMouseOver, setIsMouseOver] = useState(false);
useEffect(() => {
if (cardRef.current) {
const { left, width: localWidth } = cardRef.current.getBoundingClientRect();
setLeft(left);
setLocalWidth(localWidth);
}
}, []);
function mouseMoveHandler(event: any) {
event.preventDefault();
const { clientX } = event;
if (cardRef.current) {
const relativeX = clientX - left;
setWidthPercentage((relativeX / localWidth) * 100);
}
}
function mouseLeaveHandler() {
setIsMouseOver(false);
setWidthPercentage(0);
}
function mouseEnterHandler() {
setIsMouseOver(true);
}
function touchMoveHandler(event: React.TouchEvent<HTMLDivElement>) {
event.preventDefault();
const clientX = event.touches[0]!.clientX;
if (cardRef.current) {
const relativeX = clientX - left;
setWidthPercentage((relativeX / localWidth) * 100);
}
}
const rotateDeg = (widthPercentage - 50) * 0.1;
return (
<div
onMouseEnter={mouseEnterHandler}
onMouseLeave={mouseLeaveHandler}
onMouseMove={mouseMoveHandler}
onTouchStart={mouseEnterHandler}
onTouchEnd={mouseLeaveHandler}
onTouchMove={touchMoveHandler}
ref={cardRef}
className={cn(
"relative w-[40rem] overflow-hidden rounded-lg border border-white/[0.08] bg-[#1d1c20] p-8",
className
)}
>
{children}
<div className="relative flex h-40 items-center overflow-hidden">
<motion.div
style={{
width: "100%",
}}
animate={
isMouseOver
? {
opacity: widthPercentage > 0 ? 1 : 0,
clipPath: `inset(0 ${100 - widthPercentage}% 0 0)`,
}
: {
clipPath: `inset(0 ${100 - widthPercentage}% 0 0)`,
}
}
transition={isMouseOver ? { duration: 0 } : { duration: 0.4 }}
className="absolute z-20 bg-[#1d1c20] will-change-transform"
>
<p
style={{
textShadow: "4px 4px 15px rgba(0,0,0,0.5)",
}}
className="bg-gradient-to-b from-white to-neutral-300 bg-clip-text py-10 text-base font-bold text-transparent text-white sm:text-[3rem]"
>
{revealText}
</p>
</motion.div>
<motion.div
animate={{
left: `${widthPercentage}%`,
rotate: `${rotateDeg}deg`,
opacity: widthPercentage > 0 ? 1 : 0,
}}
transition={isMouseOver ? { duration: 0 } : { duration: 0.4 }}
className="absolute z-50 h-40 w-[8px] bg-gradient-to-b from-transparent via-neutral-800 to-transparent will-change-transform"
></motion.div>
<div className="overflow-hidden [mask-image:linear-gradient(to_bottom,transparent,white,transparent)]">
<p className="bg-[#323238] bg-clip-text py-10 text-base font-bold text-transparent sm:text-[3rem]">
{text}
</p>
<MemoizedStars />
</div>
</div>
</div>
);
};
export const TextRevealCardTitle = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return <h2 className={twMerge("mb-2 text-lg text-white", className)}>{children}</h2>;
};
export const TextRevealCardDescription = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return <p className={twMerge("text-sm text-[#a9a9a9]", className)}>{children}</p>;
};
const Stars = () => {
const randomMove = () => Math.random() * 4 - 2;
const randomOpacity = () => Math.random();
const random = () => Math.random();
return (
<div className="absolute inset-0">
{[...Array(80)].map((_, i) => (
<motion.span
key={`star-${i}`}
animate={{
top: `calc(${random() * 100}% + ${randomMove()}px)`,
left: `calc(${random() * 100}% + ${randomMove()}px)`,
opacity: randomOpacity(),
scale: [1, 1.2, 0],
}}
transition={{
duration: random() * 10 + 20,
repeat: Infinity,
ease: "linear",
}}
style={{
position: "absolute",
top: `${random() * 100}%`,
left: `${random() * 100}%`,
width: `2px`,
height: `2px`,
backgroundColor: "white",
borderRadius: "50%",
zIndex: 1,
}}
className="inline-block"
></motion.span>
))}
</div>
);
};
export const MemoizedStars = memo(Stars);
+79
View File
@@ -0,0 +1,79 @@
"use client";
import { useScroll, useTransform, motion } from "motion/react";
import React, { useEffect, useRef, useState } from "react";
interface TimelineEntry {
title: string;
content: React.ReactNode;
}
export const Timeline = ({ data }: { data: TimelineEntry[] }) => {
const ref = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState(0);
useEffect(() => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
setHeight(rect.height);
}
}, [ref]);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start 10%", "end 50%"],
});
const heightTransform = useTransform(scrollYProgress, [0, 1], [0, height]);
const opacityTransform = useTransform(scrollYProgress, [0, 0.1], [0, 1]);
return (
<div className="w-full bg-white font-sans md:px-10 dark:bg-neutral-950" ref={containerRef}>
<div className="mx-auto max-w-7xl px-4 py-20 md:px-8 lg:px-10">
<h2 className="mb-4 max-w-4xl text-lg text-black md:text-4xl dark:text-white">
Changelog from my journey
</h2>
<p className="max-w-sm text-sm text-neutral-700 md:text-base dark:text-neutral-300">
I&apos;ve been working on Aceternity for the past 2 years. Here&apos;s a timeline of my
journey.
</p>
</div>
<div ref={ref} className="relative mx-auto max-w-7xl pb-20">
{data.map((item, index) => (
<div key={index} className="flex justify-start pt-10 md:gap-10 md:pt-40">
<div className="sticky top-40 z-40 flex max-w-xs flex-col items-center self-start md:w-full md:flex-row lg:max-w-sm">
<div className="absolute left-3 flex h-10 w-10 items-center justify-center rounded-full bg-white md:left-3 dark:bg-black">
<div className="h-4 w-4 rounded-full border border-neutral-300 bg-neutral-200 p-2 dark:border-neutral-700 dark:bg-neutral-800" />
</div>
<h3 className="hidden text-xl font-bold text-neutral-500 md:block md:pl-20 md:text-5xl dark:text-neutral-500">
{item.title}
</h3>
</div>
<div className="relative w-full pr-4 pl-20 md:pl-4">
<h3 className="mb-4 block text-left text-2xl font-bold text-neutral-500 md:hidden dark:text-neutral-500">
{item.title}
</h3>
{item.content}{" "}
</div>
</div>
))}
<div
style={{
height: height + "px",
}}
className="absolute top-0 left-8 w-[2px] overflow-hidden bg-[linear-gradient(to_bottom,var(--tw-gradient-stops))] from-transparent from-[0%] via-neutral-200 to-transparent to-[99%] [mask-image:linear-gradient(to_bottom,transparent_0%,black_10%,black_90%,transparent_100%)] md:left-8 dark:via-neutral-700"
>
<motion.div
style={{
height: heightTransform,
opacity: opacityTransform,
}}
className="absolute inset-x-0 top-0 w-[2px] rounded-full bg-gradient-to-t from-purple-500 from-[0%] via-blue-500 via-[10%] to-transparent"
/>
</div>
</div>
</div>
);
};
+187
View File
@@ -0,0 +1,187 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react";
import { cn } from "@/lib/utils";
export const Tooltip = ({
content,
children,
containerClassName,
}: {
content: string | React.ReactNode;
children: React.ReactNode;
containerClassName?: string;
}) => {
const [isVisible, setIsVisible] = useState(false);
const [mouse, setMouse] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
const [height, setHeight] = useState(0);
const [position, setPosition] = useState<{ x: number; y: number }>({
x: 0,
y: 0,
});
const contentRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isVisible && contentRef.current) {
setHeight(contentRef.current.scrollHeight);
}
}, [isVisible, content]);
const calculatePosition = (mouseX: number, mouseY: number) => {
if (!contentRef.current || !containerRef.current) return { x: mouseX + 12, y: mouseY + 12 };
const tooltip = contentRef.current;
const container = containerRef.current;
const containerRect = container.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Get tooltip dimensions
const tooltipWidth = 240; // min-w-[15rem] = 240px
const tooltipHeight = tooltip.scrollHeight;
// Calculate absolute position relative to viewport
const absoluteX = containerRect.left + mouseX;
const absoluteY = containerRect.top + mouseY;
let finalX = mouseX + 12;
let finalY = mouseY + 12;
// Check if tooltip goes beyond right edge
if (absoluteX + 12 + tooltipWidth > viewportWidth) {
finalX = mouseX - tooltipWidth - 12;
}
// Check if tooltip goes beyond left edge
if (absoluteX + finalX < 0) {
finalX = -containerRect.left + 12;
}
// Check if tooltip goes beyond bottom edge
if (absoluteY + 12 + tooltipHeight > viewportHeight) {
finalY = mouseY - tooltipHeight - 12;
}
// Check if tooltip goes beyond top edge
if (absoluteY + finalY < 0) {
finalY = -containerRect.top + 12;
}
return { x: finalX, y: finalY };
};
const updateMousePosition = (mouseX: number, mouseY: number) => {
setMouse({ x: mouseX, y: mouseY });
const newPosition = calculatePosition(mouseX, mouseY);
setPosition(newPosition);
};
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
setIsVisible(true);
const rect = e.currentTarget.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
updateMousePosition(mouseX, mouseY);
};
const handleMouseLeave = () => {
setMouse({ x: 0, y: 0 });
setPosition({ x: 0, y: 0 });
setIsVisible(false);
};
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!isVisible) return;
const rect = e.currentTarget.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
updateMousePosition(mouseX, mouseY);
};
const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
const touch = e.touches[0];
const rect = e.currentTarget.getBoundingClientRect();
const mouseX = touch.clientX - rect.left;
const mouseY = touch.clientY - rect.top;
updateMousePosition(mouseX, mouseY);
setIsVisible(true);
};
const handleTouchEnd = () => {
// Delay hiding to allow for tap interaction
setTimeout(() => {
setIsVisible(false);
setMouse({ x: 0, y: 0 });
setPosition({ x: 0, y: 0 });
}, 2000);
};
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
// Toggle visibility on click for mobile devices
if (window.matchMedia("(hover: none)").matches) {
e.preventDefault();
if (isVisible) {
setIsVisible(false);
setMouse({ x: 0, y: 0 });
setPosition({ x: 0, y: 0 });
} else {
const rect = e.currentTarget.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
updateMousePosition(mouseX, mouseY);
setIsVisible(true);
}
}
};
// Update position when tooltip becomes visible or content changes
useEffect(() => {
if (isVisible && contentRef.current) {
const newPosition = calculatePosition(mouse.x, mouse.y);
setPosition(newPosition);
}
}, [isVisible, height, mouse.x, mouse.y]);
return (
<div
ref={containerRef}
className={cn("relative inline-block", containerClassName)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseMove={handleMouseMove}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
onClick={handleClick}
>
{children}
<AnimatePresence>
{isVisible && (
<motion.div
key={String(isVisible)}
initial={{ height: 0, opacity: 1 }}
animate={{ height, opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
type: "spring",
stiffness: 200,
damping: 20,
}}
className="pointer-events-none absolute z-50 min-w-[15rem] overflow-hidden rounded-md border border-transparent bg-white shadow-sm ring-1 shadow-black/5 ring-black/5 dark:bg-neutral-900 dark:shadow-white/10 dark:ring-white/5"
style={{
top: position.y,
left: position.x,
}}
>
<div
ref={contentRef}
className="p-2 text-sm text-neutral-600 md:p-4 dark:text-neutral-400"
>
{content}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
+108
View File
@@ -0,0 +1,108 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { motion, useTransform, useScroll, useSpring } from "motion/react";
import { cn } from "@/lib/utils";
export const TracingBeam = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const contentRef = useRef<HTMLDivElement>(null);
const [svgHeight, setSvgHeight] = useState(0);
useEffect(() => {
if (contentRef.current) {
setSvgHeight(contentRef.current.offsetHeight);
}
}, []);
const y1 = useSpring(useTransform(scrollYProgress, [0, 0.8], [50, svgHeight]), {
stiffness: 500,
damping: 90,
});
const y2 = useSpring(useTransform(scrollYProgress, [0, 1], [50, svgHeight - 200]), {
stiffness: 500,
damping: 90,
});
return (
<motion.div ref={ref} className={cn("relative mx-auto h-full w-full max-w-4xl", className)}>
<div className="absolute top-3 -left-4 md:-left-20">
<motion.div
transition={{
duration: 0.2,
delay: 0.5,
}}
animate={{
boxShadow: scrollYProgress.get() > 0 ? "none" : "rgba(0, 0, 0, 0.24) 0px 3px 8px",
}}
className="border-netural-200 ml-[27px] flex h-4 w-4 items-center justify-center rounded-full border shadow-sm"
>
<motion.div
transition={{
duration: 0.2,
delay: 0.5,
}}
animate={{
backgroundColor: scrollYProgress.get() > 0 ? "white" : "#10b981",
borderColor: scrollYProgress.get() > 0 ? "white" : "#059669",
}}
className="h-2 w-2 rounded-full border border-neutral-300 bg-white"
/>
</motion.div>
<svg
viewBox={`0 0 20 ${svgHeight}`}
width="20"
height={svgHeight} // Set the SVG height
className="ml-4 block"
aria-hidden="true"
>
<motion.path
d={`M 1 0V -36 l 18 24 V ${svgHeight * 0.8} l -18 24V ${svgHeight}`}
fill="none"
stroke="#9091A0"
strokeOpacity="0.16"
transition={{
duration: 10,
}}
></motion.path>
<motion.path
d={`M 1 0V -36 l 18 24 V ${svgHeight * 0.8} l -18 24V ${svgHeight}`}
fill="none"
stroke="url(#gradient)"
strokeWidth="1.25"
className="motion-reduce:hidden"
transition={{
duration: 10,
}}
></motion.path>
<defs>
<motion.linearGradient
id="gradient"
gradientUnits="userSpaceOnUse"
x1="0"
x2="0"
y1={y1} // set y1 for gradient
y2={y2} // set y2 for gradient
>
<stop stopColor="#18CCFC" stopOpacity="0"></stop>
<stop stopColor="#18CCFC"></stop>
<stop offset="0.325" stopColor="#6344F5"></stop>
<stop offset="1" stopColor="#AE48FF" stopOpacity="0"></stop>
</motion.linearGradient>
</defs>
</svg>
</div>
<div ref={contentRef}>{children}</div>
</motion.div>
);
};
+181
View File
@@ -0,0 +1,181 @@
"use client";
import { cn } from "@/lib/utils";
import { motion, stagger, useAnimate, useInView } from "motion/react";
import { useEffect } from "react";
export const TypewriterEffect = ({
words,
className,
cursorClassName,
}: {
words: {
text: string;
className?: string;
}[];
className?: string;
cursorClassName?: string;
}) => {
// split text inside of words into array of characters
const wordsArray = words.map((word) => {
return {
...word,
text: word.text.split(""),
};
});
const [scope, animate] = useAnimate();
const isInView = useInView(scope);
useEffect(() => {
if (isInView) {
animate(
"span",
{
display: "inline-block",
opacity: 1,
width: "fit-content",
},
{
duration: 0.3,
delay: stagger(0.1),
ease: "easeInOut",
}
);
}
}, [isInView]);
const renderWords = () => {
return (
<motion.div ref={scope} className="inline">
{wordsArray.map((word, idx) => {
return (
<div key={`word-${idx}`} className="inline-block">
{word.text.map((char, index) => (
<motion.span
initial={{}}
key={`char-${index}`}
className={cn(`hidden text-black opacity-0 dark:text-white`, word.className)}
>
{char}
</motion.span>
))}
&nbsp;
</div>
);
})}
</motion.div>
);
};
return (
<div
className={cn(
"text-center text-base font-bold sm:text-xl md:text-3xl lg:text-5xl",
className
)}
>
{renderWords()}
<motion.span
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
transition={{
duration: 0.8,
repeat: Infinity,
repeatType: "reverse",
}}
className={cn(
"inline-block h-4 w-[4px] rounded-sm bg-blue-500 md:h-6 lg:h-10",
cursorClassName
)}
></motion.span>
</div>
);
};
export const TypewriterEffectSmooth = ({
words,
className,
cursorClassName,
}: {
words: {
text: string;
className?: string;
}[];
className?: string;
cursorClassName?: string;
}) => {
// split text inside of words into array of characters
const wordsArray = words.map((word) => {
return {
...word,
text: word.text.split(""),
};
});
const renderWords = () => {
return (
<div>
{wordsArray.map((word, idx) => {
return (
<div key={`word-${idx}`} className="inline-block">
{word.text.map((char, index) => (
<span
key={`char-${index}`}
className={cn(`text-black dark:text-white`, word.className)}
>
{char}
</span>
))}
&nbsp;
</div>
);
})}
</div>
);
};
return (
<div className={cn("my-6 flex space-x-1", className)}>
<motion.div
className="overflow-hidden pb-2"
initial={{
width: "0%",
}}
whileInView={{
width: "fit-content",
}}
transition={{
duration: 2,
ease: "linear",
delay: 1,
}}
>
<div
className="lg:text:3xl text-xs font-bold sm:text-base md:text-xl xl:text-5xl"
style={{
whiteSpace: "nowrap",
}}
>
{renderWords()}{" "}
</div>{" "}
</motion.div>
<motion.span
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
transition={{
duration: 0.8,
repeat: Infinity,
repeatType: "reverse",
}}
className={cn("block h-4 w-[4px] rounded-sm bg-blue-500 sm:h-6 xl:h-12", cursorClassName)}
></motion.span>
</div>
);
};
+115
View File
@@ -0,0 +1,115 @@
"use client";
import { cn } from "@/lib/utils";
import { useEffect, useRef, useState } from "react";
import { createNoise3D } from "simplex-noise";
export const WavyBackground = ({
children,
className,
containerClassName,
colors,
waveWidth,
backgroundFill,
blur = 10,
speed = "fast",
waveOpacity = 0.5,
...props
}: {
children?: any;
className?: string;
containerClassName?: string;
colors?: string[];
waveWidth?: number;
backgroundFill?: string;
blur?: number;
speed?: "slow" | "fast";
waveOpacity?: number;
[key: string]: any;
}) => {
const noise = createNoise3D();
let w: number, h: number, nt: number, i: number, x: number, ctx: any, canvas: any;
const canvasRef = useRef<HTMLCanvasElement>(null);
const getSpeed = () => {
switch (speed) {
case "slow":
return 0.001;
case "fast":
return 0.002;
default:
return 0.001;
}
};
const init = () => {
canvas = canvasRef.current;
ctx = canvas.getContext("2d");
w = ctx.canvas.width = window.innerWidth;
h = ctx.canvas.height = window.innerHeight;
ctx.filter = `blur(${blur}px)`;
nt = 0;
window.onresize = function () {
w = ctx.canvas.width = window.innerWidth;
h = ctx.canvas.height = window.innerHeight;
ctx.filter = `blur(${blur}px)`;
};
render();
};
const waveColors = colors ?? ["#38bdf8", "#818cf8", "#c084fc", "#e879f9", "#22d3ee"];
const drawWave = (n: number) => {
nt += getSpeed();
for (i = 0; i < n; i++) {
ctx.beginPath();
ctx.lineWidth = waveWidth || 50;
ctx.strokeStyle = waveColors[i % waveColors.length];
for (x = 0; x < w; x += 5) {
const y = noise(x / 800, 0.3 * i, nt) * 100;
ctx.lineTo(x, y + h * 0.5); // adjust for height, currently at 50% of the container
}
ctx.stroke();
ctx.closePath();
}
};
let animationId: number;
const render = () => {
ctx.fillStyle = backgroundFill || "black";
ctx.globalAlpha = waveOpacity || 0.5;
ctx.fillRect(0, 0, w, h);
drawWave(5);
animationId = requestAnimationFrame(render);
};
useEffect(() => {
init();
return () => {
cancelAnimationFrame(animationId);
};
}, []);
const [isSafari, setIsSafari] = useState(false);
useEffect(() => {
// I'm sorry but i have got to support it on safari.
setIsSafari(
typeof window !== "undefined" &&
navigator.userAgent.includes("Safari") &&
!navigator.userAgent.includes("Chrome")
);
}, []);
return (
<div className={cn("flex h-screen flex-col items-center justify-center", containerClassName)}>
<canvas
className="absolute inset-0 z-0"
ref={canvasRef}
id="canvas"
style={{
...(isSafari ? { filter: `blur(${blur}px)` } : {}),
}}
></canvas>
<div className={cn("relative z-10", className)} {...props}>
{children}
</div>
</div>
);
};