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:
@@ -0,0 +1,73 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { ShowcaseGallery } from "@/components/showcase/gallery";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "UI Gallery — Aceternity",
|
||||||
|
description:
|
||||||
|
"Fifty-two Aceternity UI components vendored into the template — animated cards, beams, backgrounds, pins, marquees and more, wired for the App Router.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECTIONS = [
|
||||||
|
"3D Pin / Marquee",
|
||||||
|
"Backgrounds & Beams",
|
||||||
|
"Cards & Hover",
|
||||||
|
"Text Effects",
|
||||||
|
"Inputs & Buttons",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function UiGalleryPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col items-center bg-zinc-50 dark:bg-black">
|
||||||
|
<div className="w-full border-b border-zinc-200 bg-white/80 py-10 backdrop-blur-sm dark:border-zinc-800 dark:bg-zinc-950/80">
|
||||||
|
<div className="mx-auto flex w-full max-w-6xl flex-col gap-3 px-6">
|
||||||
|
<nav className="text-xs font-medium tracking-wide text-zinc-500 dark:text-zinc-400">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="transition-colors hover:text-zinc-900 dark:hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<span className="px-2">/</span>
|
||||||
|
<span className="text-zinc-800 dark:text-zinc-200">UI Gallery</span>
|
||||||
|
</nav>
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight text-balance text-zinc-900 sm:text-4xl dark:text-zinc-50">
|
||||||
|
Aceternity UI gallery
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-2xl text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||||
|
50 components vendored straight from the{" "}
|
||||||
|
<a
|
||||||
|
href="https://ui.aceternity.com/components"
|
||||||
|
className="font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 hover:decoration-zinc-500 dark:text-zinc-200 dark:decoration-zinc-600"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Aceternity registry
|
||||||
|
</a>{" "}
|
||||||
|
and wired for this template. Everything below is live — hover, click and scroll.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 pt-1">
|
||||||
|
{SECTIONS.map((s) => (
|
||||||
|
<a
|
||||||
|
key={s}
|
||||||
|
href={`#${s.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`}
|
||||||
|
className="rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-300 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400 dark:hover:border-zinc-700 dark:hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main className="w-full max-w-6xl flex-1 px-6 py-12">
|
||||||
|
<Suspense
|
||||||
|
fallback={<div className="py-24 text-center text-sm text-zinc-400">Loading gallery…</div>}
|
||||||
|
>
|
||||||
|
<ShowcaseGallery />
|
||||||
|
</Suspense>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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]);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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"> </span>
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
@@ -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've been working on Aceternity for the past 2 years. Here'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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</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>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user