Compare commits
90
Commits
ea02e5d358
...
f1c7b09f09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1c7b09f09 | ||
|
|
dbf6cfbc12 | ||
|
|
02b4437eb3 | ||
|
|
b1238d4363 | ||
|
|
94a0328cad | ||
|
|
72a49fc2d0 | ||
|
|
e6cbdcc51d | ||
|
|
9a9880d865 | ||
|
|
960eaabe42 | ||
|
|
43dad88aea | ||
|
|
5fe2746879 | ||
|
|
e288be64da | ||
|
|
b4a72b7154 | ||
|
|
32b561a96b | ||
|
|
e75444ba52 | ||
|
|
6270c2701d | ||
|
|
db07b5ae0c | ||
|
|
6f4a021785 | ||
|
|
142a814039 | ||
|
|
5df8306909 | ||
|
|
b0098c9270 | ||
|
|
29cec47928 | ||
|
|
47da9bb340 | ||
|
|
e5d3a319a8 | ||
|
|
f5b4f6e2d0 | ||
|
|
57f77a3373 | ||
|
|
dae0baea3b | ||
|
|
3ce2b9662c | ||
|
|
c5a5611e10 | ||
|
|
c4ff57876a | ||
|
|
2e82223689 | ||
|
|
738f533d4c | ||
|
|
b1045c1dd2 | ||
|
|
7df7575a05 | ||
|
|
0b4a05e3d6 | ||
|
|
c09f187465 | ||
|
|
f5a05ef4a1 | ||
|
|
0810ff1373 | ||
|
|
fb91f3df90 | ||
|
|
c9abfc4815 | ||
|
|
40b6fd3cfc | ||
|
|
8a83096ce6 | ||
|
|
be8201b60a | ||
|
|
e87d41e08a | ||
|
|
57a0891224 | ||
|
|
0bd9e49964 | ||
|
|
186c8b172b | ||
|
|
456fc31770 | ||
|
|
145c79c8b2 | ||
|
|
0b634a0a2a | ||
|
|
a2f9bc0c35 | ||
|
|
9abbe01a7b | ||
|
|
72c4fce25e | ||
|
|
d52604971d | ||
|
|
66c8b90d79 | ||
|
|
978db63a95 | ||
|
|
dcc6fc2c0a | ||
|
|
29b9a6618c | ||
|
|
caf9f4dd5e | ||
|
|
a6a9d1e236 | ||
|
|
9a265a760d | ||
|
|
57cdb609b8 | ||
|
|
5b51b2f6e9 | ||
|
|
bd4581ea2b | ||
|
|
5f9dd1a5c4 | ||
|
|
f7e621ef66 | ||
|
|
2e19b72ce2 | ||
|
|
3f1b11496e | ||
|
|
8a46d734a9 | ||
|
|
943126f6b3 | ||
|
|
d13b984ac4 | ||
|
|
78ed092e1c | ||
|
|
692d54bab1 | ||
|
|
bf75d9deef | ||
|
|
bc64b74a34 | ||
|
|
e057dd50c8 | ||
|
|
d471418c8f | ||
|
|
2bf524ff82 | ||
|
|
2afe84e608 | ||
|
|
c17aaeb9f1 | ||
|
|
cf3c6324c8 | ||
|
|
587c7137c5 | ||
|
|
79b53addf4 | ||
|
|
74ebd6bc10 | ||
|
|
d04b3d2b22 | ||
|
|
2c5b865c66 | ||
|
|
a70e08ac33 | ||
|
|
31ba33ee4d | ||
|
|
99718c8698 | ||
|
|
afeca88f80 |
@@ -1,16 +1,53 @@
|
||||
name: Dependabot Auto-Merge
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
checks: read
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Verify CI check-runs before enabling auto-merge
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const sha = context.payload.pull_request.head.sha;
|
||||
|
||||
// Wait for in-progress runs (with timeout), then collect conclusions
|
||||
const requiredContext = "lint + typecheck + test + build";
|
||||
let conclusion = null;
|
||||
for (let attempt = 0; attempt < 60; attempt++) {
|
||||
const { data: checks } = await github.rest.checks.listForRef({
|
||||
owner, repo, ref: sha,
|
||||
});
|
||||
const match = checks.check_runs.find(
|
||||
(r) => r.name === requiredContext || r.output?.title === requiredContext
|
||||
);
|
||||
if (match && match.status === "completed") {
|
||||
conclusion = match.conclusion;
|
||||
break;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 10000));
|
||||
}
|
||||
if (!conclusion) {
|
||||
core.setFailed(`Required check "${requiredContext}" never completed within timeout.`);
|
||||
return;
|
||||
}
|
||||
if (conclusion !== "success") {
|
||||
core.setFailed(`Required check "${requiredContext}" concluded ${conclusion}. NOT merging.`);
|
||||
return;
|
||||
}
|
||||
core.info(`Check "${requiredContext}" passed — safe to enable auto-merge.`);
|
||||
|
||||
- name: Enable auto-merge for Dependabot PR
|
||||
if: success()
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
|
||||
@@ -5,6 +5,8 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "dependencies"
|
||||
groups:
|
||||
production:
|
||||
dependency-type: "production"
|
||||
@@ -16,3 +18,40 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
ignore:
|
||||
# ---- Toolchain pins: semver-major bumps break the lint/typecheck stack ----
|
||||
# ESLint 10 dropped rule-context getFilename(); eslint-plugin-react (^7.37.x)
|
||||
# peer range caps at eslint ^9.7 -> react/display-name crash at load.
|
||||
- dependency-name: "eslint"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# typescript-eslint 8.x peer range is ">=4.8.4 <6.1.0" -> TS 7 hard-refuses.
|
||||
- dependency-name: "typescript"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# @tsparticles v4 changed its exported API; the vendored SparklesCore from the
|
||||
# Aceternity registry targets the v3 API (initParticlesEngine, IEffect.fill...).
|
||||
# NOTE: dependabot `ignore` does NOT support wildcards (only `groups` does) —
|
||||
# list each exact scoped package.
|
||||
- dependency-name: "@tsparticles/react"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "@tsparticles/engine"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "@tsparticles/slim"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# eslint-config-next majors follow Next.js majors and can pull incompatible
|
||||
# plugin versions; framework majors are deliberate, not auto-bumps.
|
||||
- dependency-name: "eslint-config-next"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# eslint-plugin-react 8.x targets ESLint 10, which conflicts with the
|
||||
# pinned eslint 9 (and the vendored components' validated toolchain).
|
||||
- dependency-name: "eslint-plugin-react"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
actions:
|
||||
patterns: ["*"]
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
@@ -16,6 +16,24 @@ lint/typecheck/test/build pipeline.
|
||||
- **Markdown:** react-markdown + remark-gfm + shiki (server-side highlighting)
|
||||
- **Misc:** zustand, nuqs, sonner, cmdk, vaul, embla-carousel-react, date-fns,
|
||||
next-themes, recharts
|
||||
- **Motion effects:** 48 Aceternity UI effects vendored as source in
|
||||
`components/ui/` (motion + @tabler/icons-react + @tsparticles + simplex-noise),
|
||||
showcased live at `/ui`
|
||||
|
||||
## UI gallery (`/ui`)
|
||||
|
||||
48 Aceternity effects (3d-pin, bento-grid, sparkles, tracing-beam, …) vendored
|
||||
verbatim from the [Aceternity registry](https://ui.aceternity.com/components)
|
||||
and demoed live in `components/showcase/gallery.tsx`. The shadcn Base-Nova set
|
||||
(accordions, dialogs, tables, …) ships alongside in the same `components/ui/`
|
||||
dir — 110 files total.
|
||||
|
||||
| Rule | Detail |
|
||||
| ------------- | ----------------------------------------------------------------------- |
|
||||
| Source | Registry JSON, verbatim + `motion/react` / `cn` import fixes |
|
||||
| Rename | Aceternity tabs → `animated-tabs.tsx` (collides with shadcn `tabs.tsx`) |
|
||||
| Skipped | three.js trio, apple-cards-carousel (heavy deps, out of scope) |
|
||||
| Overlay demos | `MultiStepLoader` demo is state-gated — never hardcode `loading` |
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -51,7 +69,8 @@ Git hooks (husky + lint-staged) run ESLint + Prettier on staged files.
|
||||
|
||||
```
|
||||
app/
|
||||
page.tsx # landing page
|
||||
page.tsx # landing page (HomeHero: Cover + Sparkles + Spotlight)
|
||||
ui/ # /ui gallery — 48 Aceternity effects, live demos
|
||||
demo/ # wiring example: markdown+shiki, form, table, session
|
||||
protected/ # route guarded by middleware (better-auth session)
|
||||
error.tsx / loading.tsx / not-found.tsx
|
||||
|
||||
+6
-2
@@ -14,8 +14,12 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: {
|
||||
default: "Next.js Template — Aceternity + shadcn",
|
||||
template: "%s — Next.js Template",
|
||||
},
|
||||
description:
|
||||
"Production-ready Next.js 16 starter on Bun with 48 vendored Aceternity UI effects, shadcn Base-Nova components, Tailwind v4, motion, better-auth and a typed API client.",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
|
||||
+2
-73
@@ -1,76 +1,5 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { HomeHero } from "@/components/home-hero";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex w-full max-w-3xl flex-1 flex-col items-center justify-between bg-white px-16 py-32 sm:items-start dark:bg-black">
|
||||
<Image
|
||||
className="h-5 w-[100px] dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl leading-10 font-semibold tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the{" "}
|
||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
||||
page.tsx
|
||||
</code>{" "}
|
||||
file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
<Link
|
||||
href="/demo"
|
||||
className="rounded-full border border-black/[.08] bg-foreground px-5 py-2 text-sm font-medium text-background transition-colors hover:bg-[#383838] dark:border-white/[.145] dark:hover:bg-[#1a1a1a]"
|
||||
>
|
||||
View the demo wiring →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] md:w-[158px] dark:hover:bg-[#ccc]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="h-[14px] w-4 dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={14}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] md:w-[158px] dark:border-white/[.145] dark:hover:bg-[#1a1a1a]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
return <HomeHero />;
|
||||
}
|
||||
|
||||
@@ -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 + shadcn",
|
||||
description:
|
||||
"Forty-eight Aceternity UI effects vendored into the template — animated cards, beams, backgrounds, pins, marquees and more, wired for the App Router, alongside the full shadcn Base-Nova set.",
|
||||
};
|
||||
|
||||
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-zinc-950 dark:bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.04),transparent_55%)]">
|
||||
<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">
|
||||
48 Aceternity effects 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,68 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion } from "motion/react";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { MagneticButton } from "@/components/ui/magnetic-button";
|
||||
import { SparklesCore } from "@/components/ui/sparkles";
|
||||
import { Spotlight } from "@/components/ui/spotlight";
|
||||
|
||||
export function HomeHero() {
|
||||
return (
|
||||
<div className="relative flex w-full flex-1 flex-col items-center justify-center overflow-hidden bg-zinc-50 antialiased dark:bg-zinc-950 dark:bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.06),transparent_60%)]">
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
<Spotlight className="-top-40 left-0 md:-top-20 md:left-60" fill="white" />
|
||||
</div>
|
||||
|
||||
{/* Sparkles fill the hero background */}
|
||||
<div className="absolute inset-0 z-0 h-full w-full">
|
||||
<SparklesCore
|
||||
id="home-sparkles"
|
||||
background="transparent"
|
||||
minSize={0.6}
|
||||
maxSize={1.4}
|
||||
particleDensity={60}
|
||||
particleColor="#71717a"
|
||||
className="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.main
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, ease: "easeOut" }}
|
||||
className="relative z-10 flex w-full max-w-4xl flex-col items-center gap-8 px-6 py-24 text-center"
|
||||
>
|
||||
<span className="rounded-full border border-zinc-200 bg-white/70 px-3 py-1 text-xs font-medium tracking-wide text-zinc-500 backdrop-blur dark:border-zinc-800 dark:bg-zinc-900/70 dark:text-zinc-400">
|
||||
Next.js 16 · Tailwind v4 · motion · 48 Aceternity effects + shadcn
|
||||
</span>
|
||||
|
||||
<h1 className="max-w-3xl text-4xl leading-tight font-semibold tracking-tight text-zinc-900 sm:text-6xl dark:text-white">
|
||||
A template that ships with <Cover>animated UI out of the box.</Cover>
|
||||
</h1>
|
||||
|
||||
<p className="max-w-xl text-base leading-7 text-zinc-600 dark:text-zinc-300">
|
||||
Aceternity components vendored as source, Tailwind v4 native, zero-warning lint. Every
|
||||
effect below is live — build a landing page without starting from scratch.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-4">
|
||||
<Link href="/ui" passHref>
|
||||
<MagneticButton strength={0.5} maxDistance={60}>
|
||||
<span className="inline-flex items-center gap-2 rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200">
|
||||
Browse the gallery
|
||||
<span aria-hidden>→</span>
|
||||
</span>
|
||||
</MagneticButton>
|
||||
</Link>
|
||||
<Link
|
||||
href="/demo"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-zinc-300 px-6 py-3 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-900"
|
||||
>
|
||||
View the demo wiring
|
||||
</Link>
|
||||
</div>
|
||||
</motion.main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,733 @@
|
||||
"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";
|
||||
import { Tabs } from "@/components/ui/animated-tabs";
|
||||
import { WavyBackground } from "@/components/ui/wavy-background";
|
||||
import { Boxes } from "@/components/ui/background-boxes";
|
||||
import { Spotlight } from "@/components/ui/spotlight";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { TextGenerateEffect } from "@/components/ui/text-generate-effect";
|
||||
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
|
||||
import { SquigglyText } from "@/components/ui/squiggly-text";
|
||||
import {
|
||||
TextRevealCard,
|
||||
TextRevealCardTitle,
|
||||
TextRevealCardDescription,
|
||||
} from "@/components/ui/text-reveal-card";
|
||||
import { Meteors } from "@/components/ui/meteors";
|
||||
import { GlareCard } from "@/components/ui/glare-card";
|
||||
import { MagneticButton } from "@/components/ui/magnetic-button";
|
||||
import { GooeyInput } from "@/components/ui/gooey-input";
|
||||
import { Notch } from "@/components/ui/notch";
|
||||
import { FloatingDock } from "@/components/ui/floating-dock";
|
||||
import { HeroParallax } from "@/components/ui/hero-parallax";
|
||||
import { ParallaxScroll } from "@/components/ui/parallax-scroll";
|
||||
import { ContainerScroll } from "@/components/ui/container-scroll-animation";
|
||||
import { StickyScroll } from "@/components/ui/sticky-scroll-reveal";
|
||||
import { Timeline } from "@/components/ui/timeline";
|
||||
import { FocusCards } from "@/components/ui/focus-cards";
|
||||
import { Tooltip } from "@/components/ui/tooltip-card";
|
||||
import { HoverEffect } from "@/components/ui/card-hover-effect";
|
||||
import { BackgroundGradient } from "@/components/ui/background-gradient";
|
||||
import { MultiStepLoader } from "@/components/ui/multi-step-loader";
|
||||
import {
|
||||
ModalProvider,
|
||||
Modal,
|
||||
ModalTrigger,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from "@/components/ui/animated-modal";
|
||||
import { MaskContainer } from "@/components/ui/svg-mask-effect";
|
||||
import { GoogleGeminiEffect } from "@/components/ui/google-gemini-effect";
|
||||
import { IconHome, IconUser, IconSettings, IconMoon, IconSun } from "@tabler/icons-react";
|
||||
import { useScroll, useTransform, MotionValue } from "motion/react";
|
||||
import { useState } from "react";
|
||||
|
||||
function MultiStepLoaderDemo() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<MultiStepLoader
|
||||
loading={loading}
|
||||
loadingStates={[
|
||||
{ text: "Fetching" },
|
||||
{ text: "Compiling" },
|
||||
{ text: "Rendering" },
|
||||
{ text: "Done" },
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
setTimeout(() => setLoading(false), 4000);
|
||||
}}
|
||||
className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
||||
>
|
||||
{loading ? "Loading…" : "Run loader"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<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>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="beams2" index="05" title="Waves, boxes & spotlights" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card label="wavy-background" className="md:col-span-2">
|
||||
<WavyBackground className="w-full rounded-xl py-16">
|
||||
<p className="relative z-10 text-2xl font-bold text-white">WavyBackground</p>
|
||||
<p className="relative z-10 text-sm text-zinc-300">
|
||||
Animated SVG waves behind content.
|
||||
</p>
|
||||
</WavyBackground>
|
||||
</Card>
|
||||
<Card label="background-boxes">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<Boxes className="absolute inset-0" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-300">
|
||||
Boxes — grid dots pattern
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="spotlight">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<Spotlight className="-top-20 left-0" fill="white" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-200">Spotlight</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="background-gradient" className="md:col-span-2">
|
||||
<BackgroundGradient className="rounded-xl p-6">
|
||||
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">
|
||||
BackgroundGradient — animated border glow
|
||||
</p>
|
||||
</BackgroundGradient>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="text2" index="06" title="More text effects" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Card label="cover">
|
||||
<p className="text-center text-2xl font-bold text-zinc-900 dark:text-white">
|
||||
Outline <Cover>Cover</Cover> underline
|
||||
</p>
|
||||
</Card>
|
||||
<Card label="text-generate-effect">
|
||||
<TextGenerateEffect words="Text that generates itself, character by character." />
|
||||
</Card>
|
||||
<Card label="text-hover-effect">
|
||||
<TextHoverEffect text="HOVER" />
|
||||
</Card>
|
||||
<Card label="squiggly-text">
|
||||
<SquigglyText className="text-2xl font-bold text-zinc-900 dark:text-white">
|
||||
SquigglyText wobble
|
||||
</SquigglyText>
|
||||
</Card>
|
||||
<Card label="text-reveal-card" className="md:col-span-2">
|
||||
<TextRevealCard text="Hover to reveal" revealText="A secret message 👀">
|
||||
<TextRevealCardTitle>TextRevealCard</TextRevealCardTitle>
|
||||
<TextRevealCardDescription>
|
||||
The card's cyan gradient sweeps in on hover.
|
||||
</TextRevealCardDescription>
|
||||
</TextRevealCard>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="cards2" index="07" title="Cards, meteors & buttons" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card label="meteors">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<Meteors number={20} className="absolute inset-0" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-300">Meteors</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="glare-card">
|
||||
<GlareCard className="flex h-full min-h-[240px] items-center justify-center rounded-xl">
|
||||
<p className="text-sm font-medium text-white">GlareCard</p>
|
||||
</GlareCard>
|
||||
</Card>
|
||||
<Card label="magnetic-button">
|
||||
<MagneticButton>
|
||||
<span className="inline-flex items-center gap-2 rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white dark:bg-white dark:text-zinc-900">
|
||||
MagneticButton
|
||||
</span>
|
||||
</MagneticButton>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="interaction" index="08" title="Inputs, docks & modals" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Card label="gooey-input">
|
||||
<GooeyInput placeholder="Type to search…" />
|
||||
</Card>
|
||||
<Card label="notch">
|
||||
<Notch
|
||||
items={[
|
||||
{
|
||||
id: "theme",
|
||||
label: "Theme",
|
||||
options: [
|
||||
{ id: "light", label: "Light", icon: <IconSun className="h-4 w-4" /> },
|
||||
{ id: "dark", label: "Dark", icon: <IconMoon className="h-4 w-4" /> },
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="floating-dock" className="md:col-span-2">
|
||||
<FloatingDock
|
||||
items={[
|
||||
{ title: "Home", icon: <IconHome className="h-5 w-5" />, href: "#" },
|
||||
{ title: "Profile", icon: <IconUser className="h-5 w-5" />, href: "#" },
|
||||
{ title: "Settings", icon: <IconSettings className="h-5 w-5" />, href: "#" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="animated-modal" className="md:col-span-2">
|
||||
<ModalProvider>
|
||||
<Modal>
|
||||
<ModalTrigger>
|
||||
<span className="cursor-pointer rounded-full bg-zinc-900 px-5 py-2 text-sm font-medium text-white dark:bg-white dark:text-zinc-900">
|
||||
Open animated modal
|
||||
</span>
|
||||
</ModalTrigger>
|
||||
<ModalBody>
|
||||
<ModalContent>
|
||||
<p className="text-center text-2xl text-zinc-900 dark:text-white">
|
||||
Animated Modal
|
||||
</p>
|
||||
<p className="mt-2 text-center text-sm text-zinc-500 dark:text-zinc-400">
|
||||
The modal scales and fades, with a fancy backdrop.
|
||||
</p>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<button className="rounded-full bg-zinc-900 px-5 py-2 text-sm text-white dark:bg-white dark:text-zinc-900">
|
||||
Close
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</ModalProvider>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="scroll" index="09" title="Scroll & parallax" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6">
|
||||
<Card label="sticky-scroll-reveal">
|
||||
<StickyScroll
|
||||
content={[
|
||||
{
|
||||
title: "StickyScroll",
|
||||
description: "Content sticks while you scroll; titles swap.",
|
||||
},
|
||||
{
|
||||
title: "Second block",
|
||||
description: "The next item rises as the previous fades.",
|
||||
},
|
||||
{
|
||||
title: "Third block",
|
||||
description: "Great for feature lists or onboarding.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="container-scroll-animation">
|
||||
<ContainerScroll
|
||||
titleComponent={
|
||||
<p className="text-2xl font-bold text-zinc-900 dark:text-white">ContainerScroll</p>
|
||||
}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center rounded-xl bg-zinc-100 p-8 dark:bg-zinc-800">
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300">
|
||||
The card scales up as you scroll past.
|
||||
</p>
|
||||
</div>
|
||||
</ContainerScroll>
|
||||
</Card>
|
||||
<Card label="timeline">
|
||||
<Timeline
|
||||
data={[
|
||||
{
|
||||
title: "2026 · Phase 1",
|
||||
content: <>Vendor registry components into the template.</>,
|
||||
},
|
||||
{ title: "2026 · Phase 2", content: <>Showcase gallery + home hero upgrade.</> },
|
||||
{
|
||||
title: "2026 · Phase 3",
|
||||
content: <>Zero-warning toolchain, green CI, auto-deploy.</>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="hover-scroll" index="10" title="Hover cards, parallax & masking" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6">
|
||||
<Card label="card-hover-effect">
|
||||
<HoverEffect
|
||||
className="w-full"
|
||||
items={[
|
||||
{ title: "HoverEffect", description: "Cards lift & glow on hover.", link: "#" },
|
||||
{ title: "Second card", description: "The grid is responsive.", link: "#" },
|
||||
{ title: "Third card", description: "Click to follow the link.", link: "#" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="focus-cards">
|
||||
<FocusCards
|
||||
cards={[
|
||||
{ title: "FocusCards", src: "/vercel.svg" },
|
||||
{ title: "Second", src: "/next.svg" },
|
||||
{ title: "Third", src: "/vercel.svg" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="tooltip-card">
|
||||
<Tooltip content={<p className="text-xs text-zinc-300">Tooltip on hover</p>}>
|
||||
<span className="cursor-pointer text-sm font-medium text-zinc-700 dark:text-zinc-200">
|
||||
Hover this for a tooltip
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Card>
|
||||
<Card label="multi-step-loader">
|
||||
<MultiStepLoaderDemo />
|
||||
</Card>
|
||||
<Card label="svg-mask-effect">
|
||||
<MaskContainer
|
||||
revealText={<p className="text-4xl font-bold text-zinc-300">Revealed!</p>}
|
||||
className="h-[16rem]"
|
||||
>
|
||||
<p className="text-4xl font-bold text-zinc-900 dark:text-white">
|
||||
Hover over this masked block
|
||||
</p>
|
||||
</MaskContainer>
|
||||
</Card>
|
||||
<Card label="hero-parallax">
|
||||
<HeroParallax
|
||||
products={[
|
||||
{ title: "Next", link: "#", thumbnail: "/next.svg" },
|
||||
{ title: "Vercel", link: "#", thumbnail: "/vercel.svg" },
|
||||
{ title: "Globe", link: "#", thumbnail: "/globe.svg" },
|
||||
{ title: "Window", link: "#", thumbnail: "/window.svg" },
|
||||
{ title: "File", link: "#", thumbnail: "/file.svg" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="parallax-scroll">
|
||||
<ParallaxScroll
|
||||
images={[
|
||||
"/next.svg",
|
||||
"/vercel.svg",
|
||||
"/globe.svg",
|
||||
"/window.svg",
|
||||
"/file.svg",
|
||||
"/next.svg",
|
||||
]}
|
||||
className="w-full"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="advanced" index="11" title="Tabs & gemini" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6">
|
||||
<Card label="animated-tabs">
|
||||
<Tabs
|
||||
tabs={[
|
||||
{
|
||||
title: "Overview",
|
||||
value: "overview",
|
||||
content: <p className="p-4 text-sm">Overview tab</p>,
|
||||
},
|
||||
{
|
||||
title: "Details",
|
||||
value: "details",
|
||||
content: <p className="p-4 text-sm">Details tab</p>,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
value: "settings",
|
||||
content: <p className="p-4 text-sm">Settings tab</p>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<GeminiDemo />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GeminiDemo() {
|
||||
const { scrollYProgress } = useScroll();
|
||||
const pathLengths: MotionValue<number>[] = [
|
||||
useTransform(scrollYProgress, [0, 0.2], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.2, 0.4], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.4, 0.6], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.6, 0.8], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.8, 1], [0, 1]),
|
||||
];
|
||||
return (
|
||||
<Card label="google-gemini-effect" className="overflow-visible">
|
||||
<GoogleGeminiEffect
|
||||
pathLengths={pathLengths}
|
||||
title="Google Gemini Effect"
|
||||
description="Scroll — the SVG paths draw as you scroll."
|
||||
className="w-full"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user