Compare commits
96
Commits
582d1901f6
...
main
| 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 | ||
|
|
ea02e5d358 | ||
|
|
7138e4a222 | ||
|
|
a70e08ac33 | ||
|
|
a93cecd0ee | ||
|
|
31ba33ee4d | ||
|
|
5b878d9688 | ||
|
|
99718c8698 | ||
|
|
31821f84cb | ||
|
|
afeca88f80 | ||
|
|
9ee81d06a2 |
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -8,8 +8,12 @@
|
||||
"@base-ui/react": "^1.8.0",
|
||||
"@hookform/resolvers": "^5.9.1",
|
||||
"@shadcn/react": "^0.3.1",
|
||||
"@tabler/icons-react": "^3.47.0",
|
||||
"@tanstack/react-query": "^5.103.1",
|
||||
"@tanstack/react-table": "^9.2.4",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/react": "3.0.0",
|
||||
"@tsparticles/slim": "3.9.1",
|
||||
"better-auth": "^1.7.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -20,14 +24,15 @@
|
||||
"input-otp": "^1.5.0",
|
||||
"ky": "^2.1.0",
|
||||
"lucide-react": "^1.47.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"motion": "^13.4.0",
|
||||
"next": "16.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.10.1",
|
||||
"openapi-fetch": "^0.17.0",
|
||||
"react": "19.2.8",
|
||||
"react": "19.3.0",
|
||||
"react-day-picker": "^10.0.1",
|
||||
"react-dom": "19.2.8",
|
||||
"react-dom": "19.3.0",
|
||||
"react-hook-form": "^7.88.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.12.4",
|
||||
@@ -35,6 +40,7 @@
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shadcn": "^4.21.0",
|
||||
"shiki": "^4.4.3",
|
||||
"simplex-noise": "^4.0.3",
|
||||
"sonner": "^2.0.8",
|
||||
"tailwind-merge": "^3.7.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
@@ -48,12 +54,13 @@
|
||||
"@testing-library/dom": "^10.4.2",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.3",
|
||||
"@types/node": "^20",
|
||||
"@types/node": "^26",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@vitest/coverage-v8": "^5.0.1",
|
||||
"eslint": "^9",
|
||||
"eslint": "9.39.5",
|
||||
"eslint-config-next": "16.3.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^30.1.0",
|
||||
"lint-staged": "^17.5.1",
|
||||
@@ -61,7 +68,8 @@
|
||||
"prettier": "^3.9.8",
|
||||
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "^8.70.0",
|
||||
"vitest": "^5.0.1",
|
||||
},
|
||||
},
|
||||
@@ -77,7 +85,7 @@
|
||||
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@7.0.0", "", { "dependencies": { "@csstools/css-calc": "^3.3.0", "@csstools/css-color-parser": "^4.2.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.5.2" } }, "sha512-IxddtGnnoidVSO6O/aiTueptbTThHvi2qGHu9/AWuD1J7i86Ox+jl+Y5SXcDH1RixzQlP2cjH/PjnlImZiAurQ=="],
|
||||
|
||||
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@9.2.0", "", { "dependencies": { "bidi-js": "^1.1.0", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2" } }, "sha512-qKvLetaSd/OTWif9JT0eu1SHBg3H7Nvp9SoMwMxqT5tmYyrxea/P1A5pIfGbnIJlOtm3yVeqw+ORxDOkjN1aEw=="],
|
||||
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@9.2.1", "", { "dependencies": { "bidi-js": "^1.1.0", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.3" } }, "sha512-NT4s3yZLjovPpliRpTvdsdzyPjqRqiCZj9MxnarBihbaY5MbAG7DWAJcrLlhmC7xKTNCXsTELcqB8YsqpLnUFQ=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
@@ -187,7 +195,7 @@
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
@@ -437,6 +445,10 @@
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
|
||||
|
||||
"@tabler/icons": ["@tabler/icons@3.47.0", "", {}, "sha512-aCbSemVrGvSA3w0sJi8mka6QrnQCN8wCD1jKNXXIR/h6Q4ffov/yFPyzjTULDM8ETtK4DABUZhWdYlygqy3npA=="],
|
||||
|
||||
"@tabler/icons-react": ["@tabler/icons-react@3.47.0", "", { "dependencies": { "@tabler/icons": "3.47.0" }, "peerDependencies": { "react": ">= 16" } }, "sha512-lQkDkYwqTzRNt+MYNinu+fkcIZLKjYBcI37RWkcY72doOgE/vMVFvrFrGZ2cVo2ueB3zH1dWMIhnGgi8Y5mFJA=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "Line truncated
|
||||
@@ -487,6 +499,80 @@
|
||||
|
||||
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
|
||||
|
||||
"@tsparticles/basic": ["@tsparticles/basic@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1", "@tsparticles/move-base": "3.9.1", "@tsparticles/plugin-hex-color": "3.9.1", "@tsparticles/plugin-hsl-color": "3.9.1", "@tsparticles/plugin-rgb-color": "3.9.1", "@tsparticles/shape-circle": "3.9.1", "@tsparticles/updater-color": "3.9.1", "@tsparticles/updater-opacity": "3.9.1", "@tsparticles/updater-out-modes": "3.9.1", "@tsparticles/updater-size": "3.9.1" } }, "sha512-ijr2dHMx0IQHqhKWLine truncated
|
||||
|
||||
"@tsparticles/engine": ["@tsparticles/engine@3.9.1", "", {}, "sha512-DpdgAhWMZ3Eh2gyxik8FXS6BKZ8vyea+Eu5BC4epsahqTGY9V3JGGJcXC6lRJx6cPMAx1A0FaQAojPF3v6rkmQ=="],
|
||||
|
||||
"@tsparticles/interaction-external-attract": ["@tsparticles/interaction-external-attract@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-5AJGmhzM9o4AVFV24WH5vSqMBzOXEOzIdGLIr+QJf4fRh9ZK62snsusv/ozKgs2KteRYQx+L7c5V3TqcDy2upg=="],
|
||||
|
||||
"@tsparticles/interaction-external-bounce": ["@tsparticles/interaction-external-bounce@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-bv05+h70UIHOTWeTsTI1AeAmX6R3s8nnY74Ea6p6AbQjERzPYIa0XY19nq/hA7+Nrg+EissP5zgoYYeSphr85A=="],
|
||||
|
||||
"@tsparticles/interaction-external-bubble": ["@tsparticles/interaction-external-bubble@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-tbd8ox/1GPl+zr+KyHQVV1bW88GE7OM6i4zql801YIlCDrl9wgTDdDFGIy9X7/cwTvTrCePhrfvdkUamXIribQ=="],
|
||||
|
||||
"@tsparticles/interaction-external-connect": ["@tsparticles/interaction-external-connect@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-sq8YfUNsIORjXHzzW7/AJQtfi/qDqLnYG2qOSE1WOsog39MD30RzmiOloejOkfNeUdcGUcfsDgpUuL3UhzFUOA=="],
|
||||
|
||||
"@tsparticles/interaction-external-grab": ["@tsparticles/interaction-external-grab@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-QwXza+sMMWDaMiFxd8y2tJwUK6c+nNw554+/9+tEZeTTk2fCbB0IJ7p/TH6ZGWDL0vo2muK54Njv2fEey191ow=="],
|
||||
|
||||
"@tsparticles/interaction-external-pause": ["@tsparticles/interaction-external-pause@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-Gzv4/FeNir0U/tVM9zQCqV1k+IAgaFjDU3T30M1AeAsNGh/rCITV2wnT7TOGFkbcla27m4Yxa+Fuab8+8pzm+g=="],
|
||||
|
||||
"@tsparticles/interaction-external-push": ["@tsparticles/interaction-external-push@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-GvnWF9Qy4YkZdx+WJL2iy9IcgLvzOIu3K7aLYJFsQPaxT8d9TF8WlpoMlWKnJID6H5q4JqQuMRKRyWH8aAKyQw=="],
|
||||
|
||||
"@tsparticles/interaction-external-remove": ["@tsparticles/interaction-external-remove@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-yPThm4UDWejDOWW5Qc8KnnS2EfSo5VFcJUQDWc1+Wcj17xe7vdSoiwwOORM0PmNBzdDpSKQrte/gUnoqaUMwOA=="],
|
||||
|
||||
"@tsparticles/interaction-external-repulse": ["@tsparticles/interaction-external-repulse@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-/LBppXkrMdvLHlEKWC7IykFhzrz+9nebT2fwSSFXK4plEBxDlIwnkDxd3FbVOAbnBvx4+L8+fbrEx+RvC8diAw=="],
|
||||
|
||||
"@tsparticles/interaction-external-slow": ["@tsparticles/interaction-external-slow@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-1ZYIR/udBwA9MdSCfgADsbDXKSFS0FMWuPWz7bm79g3sUxcYkihn+/hDhc6GXvNNR46V1ocJjrj0u6pAynS1KQ=="],
|
||||
|
||||
"@tsparticles/interaction-particles-attract": ["@tsparticles/interaction-particles-attract@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-CYYYowJuGwRLUixQcSU/48PTKM8fCUYThe0hXwQ+yRMLAn053VHzL7NNZzKqEIeEyt5oJoy9KcvubjKWbzMBLQ=="],
|
||||
|
||||
"@tsparticles/interaction-particles-collisions": ["@tsparticles/interaction-particles-collisions@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-ggGyjW/3v1yxvYW1IF1EMT15M6w31y5zfNNUPkqd/IXRNPYvm0Z0ayhp+FKmz70M5p0UxxPIQHTvAv9Jqnuj8w=="],
|
||||
|
||||
"@tsparticles/interaction-particles-links": ["@tsparticles/interaction-particles-links@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-MsLbMjy1vY5M5/hu/oa5OSRZAUz49H3+9EBMTIOThiX+a+vpl3sxc9AqNd9gMsPbM4WJlub8T6VBZdyvzez1Vg=="],
|
||||
|
||||
"@tsparticles/move-base": ["@tsparticles/move-base@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-X4huBS27d8srpxwOxliWPUt+NtCwY+8q/cx1DvQxyqmTA8VFCGpcHNwtqiN+9JicgzOvSuaORVqUgwlsc7h4pQ=="],
|
||||
|
||||
"@tsparticles/move-parallax": ["@tsparticles/move-parallax@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-whlOR0bVeyh6J/hvxf/QM3DqvNnITMiAQ0kro6saqSDItAVqg4pYxBfEsSOKq7EhjxNvfhhqR+pFMhp06zoCVA=="],
|
||||
|
||||
"@tsparticles/plugin-easing-quad": ["@tsparticles/plugin-easing-quad@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-C2UJOca5MTDXKUTBXj30Kiqr5UyID+xrY/LxicVWWZPczQW2bBxbIbfq9ULvzGDwBTxE2rdvIB8YFKmDYO45qw=="],
|
||||
|
||||
"@tsparticles/plugin-hex-color": ["@tsparticles/plugin-hex-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-vZgZ12AjUicJvk7AX4K2eAmKEQX/D1VEjEPFhyjbgI7A65eX72M465vVKIgNA6QArLZ1DLs7Z787LOE6GOBWsg=="],
|
||||
|
||||
"@tsparticles/plugin-hsl-color": ["@tsparticles/plugin-hsl-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-jJd1iGgRwX6eeNjc1zUXiJivaqC5UE+SC2A3/NtHwwoQrkfxGWmRHOsVyLnOBRcCPgBp/FpdDe6DIDjCMO715w=="],
|
||||
|
||||
"@tsparticles/plugin-rgb-color": ["@tsparticles/plugin-rgb-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-SBxk7f1KBfXeTnnklbE2Hx4jBgh6I6HOtxb+Os1gTp0oaghZOkWcCD2dP4QbUu7fVNCMOcApPoMNC8RTFcy9wQ=="],
|
||||
|
||||
"@tsparticles/react": ["@tsparticles/react@3.0.0", "", { "peerDependencies": { "@tsparticles/engine": "^3.0.2", "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-hjGEtTT1cwv6BcjL+GcVgH++KYs52bIuQGW3PWv7z3tMa8g0bd6RI/vWSLj7p//NZ3uTjEIeilYIUPBh7Jfq/Q=="],
|
||||
|
||||
"@tsparticles/shape-circle": ["@tsparticles/shape-circle@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-DqZFLjbuhVn99WJ+A9ajz9YON72RtCcvubzq6qfjFmtwAK7frvQeb6iDTp6Ze9FUipluxVZWVRG4vWTxi2B+/g=="],
|
||||
|
||||
"@tsparticles/shape-emoji": ["@tsparticles/shape-emoji@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-ifvY63usuT+hipgVHb8gelBHSeF6ryPnMxAAEC1RGHhhXfpSRWMtE6ybr+pSsYU52M3G9+TF84v91pSwNrb9ZQ=="],
|
||||
|
||||
"@tsparticles/shape-image": ["@tsparticles/shape-image@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-fCA5eme8VF3oX8yNVUA0l2SLDKuiZObkijb0z3Ky0qj1HUEVlAuEMhhNDNB9E2iELTrWEix9z7BFMePp2CC7AA=="],
|
||||
|
||||
"@tsparticles/shape-line": ["@tsparticles/shape-line@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-wT8NSp0N9HURyV05f371cHKcNTNqr0/cwUu6WhBzbshkYGy1KZUP9CpRIh5FCrBpTev34mEQfOXDycgfG0KiLQ=="],
|
||||
|
||||
"@tsparticles/shape-polygon": ["@tsparticles/shape-polygon@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-dA77PgZdoLwxnliH6XQM/zF0r4jhT01pw5y7XTeTqws++hg4rTLV9255k6R6eUqKq0FPSW1/WBsBIl7q/MmrqQ=="],
|
||||
|
||||
"@tsparticles/shape-square": ["@tsparticles/shape-square@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-DKGkDnRyZrAm7T2ipqNezJahSWs6xd9O5LQLe5vjrYm1qGwrFxJiQaAdlb00UNrexz1/SA7bEoIg4XKaFa7qhQ=="],
|
||||
|
||||
"@tsparticles/shape-star": ["@tsparticles/shape-star@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-kdMJpi8cdeb6vGrZVSxTG0JIjCwIenggqk0EYeKAwtOGZFBgL7eHhF2F6uu1oq8cJAbXPujEoabnLsz6mW8XaA=="],
|
||||
|
||||
"@tsparticles/slim": ["@tsparticles/slim@3.9.1", "", { "dependencies": { "@tsparticles/basic": "3.9.1", "@tsparticles/engine": "3.9.1", "@tsparticles/interaction-external-attract": "3.9.1", "@tsparticles/interaction-external-bounce": "3.9.1", "@tsparticles/interaction-external-bubble": "3.9.1", "@tsparticles/interaction-external-connect": "3.9.1", "@tsparticles/interaction-external-grab": "3.9.1", "@tsparticles/interaction-external-pause": "3.9.1", "@tsparticles/interaction-external-push": Line truncated
|
||||
|
||||
"@tsparticles/updater-color": ["@tsparticles/updater-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-XGWdscrgEMA8L5E7exsE0f8/2zHKIqnTrZymcyuFBw2DCB6BIV+5z6qaNStpxrhq3DbIxxhqqcybqeOo7+Alpg=="],
|
||||
|
||||
"@tsparticles/updater-life": ["@tsparticles/updater-life@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-Oi8aF2RIwMMsjssUkCB6t3PRpENHjdZf6cX92WNfAuqXtQphr3OMAkYFJFWkvyPFK22AVy3p/cFt6KE5zXxwAA=="],
|
||||
|
||||
"@tsparticles/updater-opacity": ["@tsparticles/updater-opacity@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-w778LQuRZJ+IoWzeRdrGykPYSSaTeWfBvLZ2XwYEkh/Ss961InOxZKIpcS6i5Kp/Zfw0fS1ZAuqeHwuj///Osw=="],
|
||||
|
||||
"@tsparticles/updater-out-modes": ["@tsparticles/updater-out-modes@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-cKQEkAwbru+hhKF+GTsfbOvuBbx2DSB25CxOdhtW2wRvDBoCnngNdLw91rs+0Cex4tgEeibkebrIKFDDE6kELg=="],
|
||||
|
||||
"@tsparticles/updater-rotate": ["@tsparticles/updater-rotate@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-9BfKaGfp28JN82MF2qs6Ae/lJr9EColMfMTHqSKljblwbpVDHte4umuwKl3VjbRt87WD9MGtla66NTUYl+WxuQ=="],
|
||||
|
||||
"@tsparticles/updater-size": ["@tsparticles/updater-size@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-3NSVs0O2ApNKZXfd+y/zNhTXSFeG1Pw4peI8e6z/q5+XLbmue9oiEwoPy/tQLaark3oNj3JU7Q903ZijPyXSzw=="],
|
||||
|
||||
"@tsparticles/updater-stroke-color": ["@tsparticles/updater-stroke-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-3x14+C2is9pZYTg9T2TiA/aM1YMq4wLdYaZDcHm3qO30DZu5oeQq0rm/6w+QOGKYY1Z3Htg9rlSUZkhTHn7eDA=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="],
|
||||
|
||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||
@@ -529,7 +615,7 @@
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
||||
"@types/node": ["@types/node@26.6.2", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g=="],
|
||||
|
||||
"@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
|
||||
|
||||
@@ -1415,6 +1501,8 @@
|
||||
|
||||
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
||||
|
||||
"mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
@@ -1537,7 +1625,7 @@
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
|
||||
"postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="],
|
||||
|
||||
@@ -1571,15 +1659,15 @@
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||
"react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
|
||||
|
||||
"react-day-picker": ["react-day-picker@10.0.1", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||
"react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.88.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-QRaLOWhX93YCnMiRfnOFRSwWXZNt8qhm2JTZwypoDvKpSffTJHmpzMXt8U6PV5UThvL3IiiLDUWe2nHMtz6Mmw=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7RLine truncated
|
||||
|
||||
@@ -1655,7 +1743,7 @@
|
||||
|
||||
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
"scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
@@ -1695,6 +1783,8 @@
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"simplex-noise": ["simplex-noise@4.0.3", "", {}, "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||
@@ -1761,7 +1851,7 @@
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"systeminformation": ["systeminformation@5.33.12", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-7x1LtTPaopmrXUJmnxW6I98YIDihwiZEETD+hzI9X/GGIghcz8r38vlZ+TAqwQTOhPuZPB1xn/AJ2GPzvn9JfQ=="],
|
||||
"systeminformation": ["systeminformation@5.33.13", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-GtyakkchTQKymM1WeAB652zi3rM1q00RKdc+jZe9KGstuc/mWUwAnQGN/RE5gVYvHIR8xYOUIUD9irVKDtA0wA=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.7.0", "", {}, "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA=="],
|
||||
|
||||
@@ -1819,7 +1909,7 @@
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.70.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.70.0", "@typescript-eslint/parser": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0", "@typescript-eslint/utils": "8.70.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw=="],
|
||||
|
||||
@@ -1827,7 +1917,7 @@
|
||||
|
||||
"undici": ["undici@8.10.2", "", {}, "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
"undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
@@ -1955,8 +2045,6 @@
|
||||
|
||||
"@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"@next/eslint-plugin-next/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
||||
|
||||
"@redocly/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
@@ -2047,8 +2135,6 @@
|
||||
|
||||
"negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||
|
||||
"next/postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
@@ -2059,10 +2145,10 @@
|
||||
|
||||
"pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
|
||||
|
||||
"pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"shadcn/cn": ["cn@0.2.6", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ=="],
|
||||
@@ -2081,6 +2167,8 @@
|
||||
|
||||
"vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightnLine truncated
|
||||
|
||||
"vite/postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
|
||||
|
||||
"vitest/tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
|
||||
|
||||
"wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||
@@ -2105,8 +2193,6 @@
|
||||
|
||||
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@next/eslint-plugin-next/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="],
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
interface AuroraBackgroundProps extends React.HTMLProps<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
showRadialGradient?: boolean;
|
||||
}
|
||||
|
||||
export const AuroraBackground = ({
|
||||
className,
|
||||
children,
|
||||
showRadialGradient = true,
|
||||
...props
|
||||
}: AuroraBackgroundProps) => {
|
||||
return (
|
||||
<main>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-bg relative flex h-[100vh] flex-col items-center justify-center bg-zinc-50 text-slate-950 dark:bg-zinc-900",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={
|
||||
{
|
||||
"--aurora":
|
||||
"repeating-linear-gradient(100deg,#3b82f6_10%,#a5b4fc_15%,#93c5fd_20%,#ddd6fe_25%,#60a5fa_30%)",
|
||||
"--dark-gradient":
|
||||
"repeating-linear-gradient(100deg,#000_0%,#000_7%,transparent_10%,transparent_12%,#000_16%)",
|
||||
"--white-gradient":
|
||||
"repeating-linear-gradient(100deg,#fff_0%,#fff_7%,transparent_10%,transparent_12%,#fff_16%)",
|
||||
|
||||
"--blue-300": "#93c5fd",
|
||||
"--blue-400": "#60a5fa",
|
||||
"--blue-500": "#3b82f6",
|
||||
"--indigo-300": "#a5b4fc",
|
||||
"--violet-200": "#ddd6fe",
|
||||
"--black": "#000",
|
||||
"--white": "#fff",
|
||||
"--transparent": "transparent",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
// I'm sorry but this is what peak developer performance looks like // trigger warning
|
||||
className={cn(
|
||||
`after:animate-aurora pointer-events-none absolute -inset-[10px] [background-image:var(--white-gradient),var(--aurora)] [background-size:300%,_200%] [background-position:50%_50%,50%_50%] opacity-50 blur-[10px] invert filter will-change-transform [--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)] [--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--trLine truncated
|
||||
|
||||
showRadialGradient &&
|
||||
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const BackgroundBeams = React.memo(({ className }: { className?: string }) => {
|
||||
const paths = [
|
||||
"M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875",
|
||||
"M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867",
|
||||
"M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859",
|
||||
"M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851",
|
||||
"M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843",
|
||||
"M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835",
|
||||
"M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827",
|
||||
"M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819",
|
||||
"M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811",
|
||||
"M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803",
|
||||
"M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795",
|
||||
"M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787",
|
||||
"M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779",
|
||||
"M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771",
|
||||
"M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763",
|
||||
"M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755",
|
||||
"M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747",
|
||||
"M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739",
|
||||
"M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731",
|
||||
"M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723",
|
||||
"M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715",
|
||||
"M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707",
|
||||
"M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699",
|
||||
"M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691",
|
||||
"M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683",
|
||||
"M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675",
|
||||
"M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667",
|
||||
"M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659",
|
||||
"M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651",
|
||||
"M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643",
|
||||
"M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635",
|
||||
"M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627",
|
||||
"M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619",
|
||||
"M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611",
|
||||
"M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603",
|
||||
"M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595",
|
||||
"M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587",
|
||||
"M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579",
|
||||
"M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571",
|
||||
"M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563",
|
||||
"M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555",
|
||||
"M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547",
|
||||
"M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539",
|
||||
"M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531",
|
||||
"M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523",
|
||||
"M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515",
|
||||
"M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507",
|
||||
"M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499",
|
||||
"M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491",
|
||||
"M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483",
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex h-full w-full items-center justify-center [mask-size:40px] [mask-repeat:no-repeat]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
className="pointer-events-none absolute z-0 h-full w-full"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 696 316"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 8Line truncated
|
||||
stroke="url(#paint0_radial_242_278)"
|
||||
strokeOpacity="0.05"
|
||||
strokeWidth="0.5"
|
||||
></path>
|
||||
|
||||
{paths.map((path, index) => (
|
||||
<motion.path
|
||||
key={`path-` + index}
|
||||
d={path}
|
||||
stroke={`url(#linearGradient-${index})`}
|
||||
strokeOpacity="0.4"
|
||||
strokeWidth="0.5"
|
||||
></motion.path>
|
||||
))}
|
||||
<defs>
|
||||
{paths.map((_path, index) => (
|
||||
<motion.linearGradient
|
||||
id={`linearGradient-${index}`}
|
||||
key={`gradient-${index}`}
|
||||
initial={{
|
||||
x1: "0%",
|
||||
x2: "0%",
|
||||
y1: "0%",
|
||||
y2: "0%",
|
||||
}}
|
||||
animate={{
|
||||
x1: ["0%", "100%"],
|
||||
x2: ["0%", "95%"],
|
||||
y1: ["0%", "100%"],
|
||||
y2: ["0%", `${93 + Math.random() * 8}%`],
|
||||
}}
|
||||
transition={{
|
||||
duration: Math.random() * 10 + 10,
|
||||
ease: "easeInOut",
|
||||
repeat: Infinity,
|
||||
delay: Math.random() * 10,
|
||||
}}
|
||||
>
|
||||
<stop stopColor="#18CCFC" stopOpacity="0"></stop>
|
||||
<stop stopColor="#18CCFC"></stop>
|
||||
<stop offset="32.5%" stopColor="#6344F5"></stop>
|
||||
<stop offset="100%" stopColor="#AE48FF" stopOpacity="0"></stop>
|
||||
</motion.linearGradient>
|
||||
))}
|
||||
|
||||
<radialGradient
|
||||
id="paint0_radial_242_278"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(352 34) rotate(90) scale(555 1560.62)"
|
||||
>
|
||||
<stop offset="0.0666667" stopColor="#d4d4d4"></stop>
|
||||
<stop offset="0.243243" stopColor="#d4d4d4"></stop>
|
||||
<stop offset="0.43594" stopColor="white" stopOpacity="0"></stop>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
BackgroundBeams.displayName = "BackgroundBeams";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useRef } from "react";
|
||||
|
||||
export const GlareCard = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
const isPointerInside = useRef(false);
|
||||
const refElement = useRef<HTMLDivElement>(null);
|
||||
const state = useRef({
|
||||
glare: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
},
|
||||
background: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
},
|
||||
rotate: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
const containerStyle = {
|
||||
"--m-x": "50%",
|
||||
"--m-y": "50%",
|
||||
"--r-x": "0deg",
|
||||
"--r-y": "0deg",
|
||||
"--bg-x": "50%",
|
||||
"--bg-y": "50%",
|
||||
"--duration": "300ms",
|
||||
"--foil-size": "100%",
|
||||
"--opacity": "0",
|
||||
"--radius": "48px",
|
||||
"--easing": "ease",
|
||||
"--transition": "var(--duration) var(--easing)",
|
||||
} as any;
|
||||
|
||||
const backgroundStyle = {
|
||||
"--step": "5%",
|
||||
"--foil-svg": `url("data:image/svg+xml,%3Csvg width='26' height='26' viewBox='0 0 26 26' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.99994 3.419C2.99994 3.419 21.6142 7.43646 22.7921 12.153C23.97 16.8695 3.41838 23.0306 3.41838 23.0306' stroke='white' stroke-width='5' stroke-miterlimit='3.86874' stroke-linecap='round' style='mix-blend-mode:darken'/%3E%3C/svg%3E")`,
|
||||
"--pattern": "var(--foil-svg) center/100% no-repeat",
|
||||
"--rainbow":
|
||||
"repeating-linear-gradient( 0deg,rgb(255,119,115) calc(var(--step) * 1),rgba(255,237,95,1) calc(var(--step) * 2),rgba(168,255,95,1) calc(var(--step) * 3),rgba(131,255,247,1) calc(var(--step) * 4),rgba(120,148,255,1) calc(var(--step) * 5),rgb(216,117,255) calc(var(--step) * 6),rgb(255,119,115) calc(var(--step) * 7) ) 0% var(--bg-y)/200% 700% no-repeat",
|
||||
"--diagonal":
|
||||
"repeating-linear-gradient( 128deg,#0e152e 0%,hsl(180,10%,60%) 3.8%,hsl(180,10%,60%) 4.5%,hsl(180,10%,60%) 5.2%,#0e152e 10%,#0e152e 12% ) var(--bg-x) var(--bg-y)/300% no-repeat",
|
||||
"--shade":
|
||||
"radial-gradient( farthest-corner circle at var(--m-x) var(--m-y),rgba(255,255,255,0.1) 12%,rgba(255,255,255,0.15) 20%,rgba(255,255,255,0.25) 120% ) var(--bg-x) var(--bg-y)/300% no-repeat",
|
||||
backgroundBlendMode: "hue, hue, hue, overlay",
|
||||
};
|
||||
|
||||
const updateStyles = () => {
|
||||
if (refElement.current) {
|
||||
console.log(state.current);
|
||||
const { background, rotate, glare } = state.current;
|
||||
refElement.current?.style.setProperty("--m-x", `${glare.x}%`);
|
||||
refElement.current?.style.setProperty("--m-y", `${glare.y}%`);
|
||||
refElement.current?.style.setProperty("--r-x", `${rotate.x}deg`);
|
||||
refElement.current?.style.setProperty("--r-y", `${rotate.y}deg`);
|
||||
refElement.current?.style.setProperty("--bg-x", `${background.x}%`);
|
||||
refElement.current?.style.setProperty("--bg-y", `${background.y}%`);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
style={containerStyle}
|
||||
className="relative isolate [aspect-ratio:17/21] w-[320px] transition-transform delay-[var(--delay)] duration-[var(--duration)] ease-[var(--easing)] will-change-transform [contain:layout_style] [perspective:600px]"
|
||||
ref={refElement}
|
||||
onPointerMove={(event) => {
|
||||
const rotateFactor = 0.4;
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const position = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
};
|
||||
const percentage = {
|
||||
x: (100 / rect.width) * position.x,
|
||||
y: (100 / rect.height) * position.y,
|
||||
};
|
||||
const delta = {
|
||||
x: percentage.x - 50,
|
||||
y: percentage.y - 50,
|
||||
};
|
||||
|
||||
const { background, rotate, glare } = state.current;
|
||||
background.x = 50 + percentage.x / 4 - 12.5;
|
||||
background.y = 50 + percentage.y / 3 - 16.67;
|
||||
rotate.x = -(delta.x / 3.5);
|
||||
rotate.y = delta.y / 2;
|
||||
rotate.x *= rotateFactor;
|
||||
rotate.y *= rotateFactor;
|
||||
glare.x = percentage.x;
|
||||
glare.y = percentage.y;
|
||||
|
||||
updateStyles();
|
||||
}}
|
||||
onPointerEnter={() => {
|
||||
isPointerInside.current = true;
|
||||
if (refElement.current) {
|
||||
setTimeout(() => {
|
||||
if (isPointerInside.current) {
|
||||
refElement.current?.style.setProperty("--duration", "0s");
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
isPointerInside.current = false;
|
||||
if (refElement.current) {
|
||||
refElement.current.style.removeProperty("--duration");
|
||||
refElement.current?.style.setProperty("--r-x", `0deg`);
|
||||
refElement.current?.style.setProperty("--r-y", `0deg`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="grid h-full origin-center [transform:rotateY(var(--r-x))_rotateX(var(--r-y))] overflow-hidden rounded-[var(--radius)] border border-slate-800 transition-transform delay-[var(--delay)] duration-[var(--duration)] ease-[var(--easing)] will-change-transform hover:filter-none hover:[--duration:200ms] hover:[--easing:linear] hover:[--opacity:0.6]">
|
||||
<div className="grid h-full w-full mix-blend-soft-light [clip-path:inset(0_0_0_0_round_var(--radius))] [grid-area:1/1]">
|
||||
<div className={cn("h-full w-full bg-slate-950", className)}>{children}</div>
|
||||
</div>
|
||||
<div className="transition-background will-change-background grid h-full w-full opacity-[var(--opacity)] mix-blend-soft-light transition-opacity delay-[var(--delay)] duration-[var(--duration)] ease-[var(--easing)] [background:radial-gradient(farthest-corner_circle_at_var(--m-x)_var(--m-y),_rgba(255,255,255,0.8)_10%,_rgba(255,255,255,0.65)_20%,_rgba(255,255,255,0)_90%)] [clip-path:inset(0_0_1px_0_round_var(--radius))] [grid-area:1/1]" />
|
||||
<div
|
||||
className="will-change-background after:grid-area-[inherit] after:bg-repeat-[inherit] after:bg-attachment-[inherit] after:bg-origin-[inherit] after:bg-clip-[inherit] relative grid h-full w-full opacity-[var(--opacity)] [background-blend-mode:hue_hue_hue_overlay] mix-blend-color-dodge transition-opacity [background:var(--pattern),_var(--rainbow),_var(--diagonal),_var(--shade)] [clip-path:inset(0_0_1px_0_round_var(--radius))] [grid-area:1/1] after:bg-[inherit] after:[background-size:varLine truncated
|
||||
style={{ ...backgroundStyle }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion, MotionValue } from "motion/react";
|
||||
|
||||
const transition = {
|
||||
duration: 0,
|
||||
ease: "linear" as const,
|
||||
};
|
||||
|
||||
export const GoogleGeminiEffect = ({
|
||||
pathLengths,
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
}: {
|
||||
pathLengths: MotionValue[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("sticky top-80", className)}>
|
||||
<p className="bg-gradient-to-b from-neutral-100 to-neutral-300 bg-clip-text pb-4 text-center text-lg font-normal text-transparent md:text-7xl">
|
||||
{title || `Build with Aceternity UI`}
|
||||
</p>
|
||||
<p className="mx-auto mt-4 max-w-lg text-center text-xs font-normal text-neutral-400 md:text-xl">
|
||||
{description ||
|
||||
`Scroll this component and see the bottom SVG come to life wow this
|
||||
works!`}
|
||||
</p>
|
||||
<div className="bg-red-transparent absolute -top-60 flex h-[890px] w-full items-center justify-center md:-top-40">
|
||||
<button className="z-30 mx-auto mt-8 w-fit rounded-full bg-white px-2 py-1 text-xs font-bold text-black md:mt-24 md:px-4 md:py-2 md:text-base">
|
||||
ui.aceternity.com
|
||||
</button>
|
||||
</div>
|
||||
<svg
|
||||
width="1440"
|
||||
height="890"
|
||||
viewBox="0 0 1440 890"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="absolute -top-60 w-full md:-top-40"
|
||||
>
|
||||
<motion.path
|
||||
d="M0 663C145.5 663 191 666.265 269 647C326.5 630 339.5 621 397.5 566C439 531.5 455 529.5 490 523C509.664 519.348 521 503.736 538 504.236C553.591 504.236 562.429 514.739 584.66 522.749C592.042 525.408 600.2 526.237 607.356 523.019C624.755 515.195 641.446 496.324 657 496.735C673.408 496.735 693.545 519.572 712.903 526.769C718.727 528.934 725.184 528.395 730.902 525.965C751.726 517.115 764.085 497.106 782 496.735C794.831 496.47 804.103 508.859 822.469 518.515C835.13 525.171 850.214 526.Line truncated
|
||||
stroke="#FFB7C5"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
initial={{
|
||||
pathLength: 0,
|
||||
}}
|
||||
style={{
|
||||
pathLength: pathLengths[0],
|
||||
}}
|
||||
transition={transition}
|
||||
/>
|
||||
<motion.path
|
||||
d="M0 587.5C147 587.5 277 587.5 310 573.5C348 563 392.5 543.5 408 535C434 523.5 426 526.235 479 515.235C494 512.729 523 510.435 534.5 512.735C554.5 516.735 555.5 523.235 576 523.735C592 523.735 616 496.735 633 497.235C648.671 497.235 661.31 515.052 684.774 524.942C692.004 527.989 700.2 528.738 707.349 525.505C724.886 517.575 741.932 498.33 757.5 498.742C773.864 498.742 791.711 520.623 810.403 527.654C816.218 529.841 822.661 529.246 828.451 526.991C849.246 518.893 861.599 502.112 879.5Line truncated
|
||||
stroke="#FFDDB7"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
initial={{
|
||||
pathLength: 0,
|
||||
}}
|
||||
style={{
|
||||
pathLength: pathLengths[1],
|
||||
}}
|
||||
transition={transition}
|
||||
/>
|
||||
<motion.path
|
||||
d="M0 514C147.5 514.333 294.5 513.735 380.5 513.735C405.976 514.94 422.849 515.228 436.37 515.123C477.503 514.803 518.631 506.605 559.508 511.197C564.04 511.706 569.162 512.524 575 513.735C588 516.433 616 521.702 627.5 519.402C647.5 515.402 659 499.235 680.5 499.235C700.5 499.235 725 529.235 742 528.735C757.654 528.735 768.77 510.583 791.793 500.59C798.991 497.465 807.16 496.777 814.423 499.745C832.335 507.064 850.418 524.648 866 524.235C882.791 524.235 902.316 509.786 921.814 505.392Line truncated
|
||||
stroke="#B1C5FF"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
initial={{
|
||||
pathLength: 0,
|
||||
}}
|
||||
style={{
|
||||
pathLength: pathLengths[2],
|
||||
}}
|
||||
transition={transition}
|
||||
/>
|
||||
<motion.path
|
||||
d="M0 438.5C150.5 438.5 261 438.318 323.5 456.5C351 464.5 387.517 484.001 423.5 494.5C447.371 501.465 472 503.735 487 507.735C503.786 512.212 504.5 516.808 523 518.735C547 521.235 564.814 501.235 584.5 501.235C604.5 501.235 626 529.069 643 528.569C658.676 528.569 672.076 511.63 695.751 501.972C703.017 499.008 711.231 498.208 718.298 501.617C735.448 509.889 751.454 529.98 767 529.569C783.364 529.569 801.211 507.687 819.903 500.657C825.718 498.469 832.141 499.104 837.992 501.194C859.178Line truncated
|
||||
stroke="#4FABFF"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
initial={{
|
||||
pathLength: 0,
|
||||
}}
|
||||
style={{
|
||||
pathLength: pathLengths[3],
|
||||
}}
|
||||
transition={transition}
|
||||
/>
|
||||
<motion.path
|
||||
d="M0.5 364C145.288 362.349 195 361.5 265.5 378C322 391.223 399.182 457.5 411 467.5C424.176 478.649 456.916 491.677 496.259 502.699C498.746 503.396 501.16 504.304 503.511 505.374C517.104 511.558 541.149 520.911 551.5 521.236C571.5 521.236 590 498.736 611.5 498.736C631.5 498.736 652.5 529.236 669.5 528.736C685.171 528.736 697.81 510.924 721.274 501.036C728.505 497.988 736.716 497.231 743.812 500.579C761.362 508.857 778.421 529.148 794 528.736C810.375 528.736 829.35 508.68 848.364 502.1Line truncated
|
||||
stroke="#076EFF"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
initial={{
|
||||
pathLength: 0,
|
||||
}}
|
||||
style={{
|
||||
pathLength: pathLengths[4],
|
||||
}}
|
||||
transition={transition}
|
||||
/>
|
||||
|
||||
{/* Gaussian blur for the background paths */}
|
||||
|
||||
<path
|
||||
d="M0 663C145.5 663 191 666.265 269 647C326.5 630 339.5 621 397.5 566C439 531.5 455 529.5 490 523C509.664 519.348 521 503.736 538 504.236C553.591 504.236 562.429 514.739 584.66 522.749C592.042 525.408 600.2 526.237 607.356 523.019C624.755 515.195 641.446 496.324 657 496.735C673.408 496.735 693.545 519.572 712.903 526.769C718.727 528.934 725.184 528.395 730.902 525.965C751.726 517.115 764.085 497.106 782 496.735C794.831 496.47 804.103 508.859 822.469 518.515C835.13 525.171 850.214 526.Line truncated
|
||||
stroke="#FFB7C5"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
pathLength={1}
|
||||
filter="url(#blurMe)"
|
||||
/>
|
||||
<path
|
||||
d="M0 587.5C147 587.5 277 587.5 310 573.5C348 563 392.5 543.5 408 535C434 523.5 426 526.235 479 515.235C494 512.729 523 510.435 534.5 512.735C554.5 516.735 555.5 523.235 576 523.735C592 523.735 616 496.735 633 497.235C648.671 497.235 661.31 515.052 684.774 524.942C692.004 527.989 700.2 528.738 707.349 525.505C724.886 517.575 741.932 498.33 757.5 498.742C773.864 498.742 791.711 520.623 810.403 527.654C816.218 529.841 822.661 529.246 828.451 526.991C849.246 518.893 861.599 502.112 879.5Line truncated
|
||||
stroke="#FFDDB7"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
pathLength={1}
|
||||
filter="url(#blurMe)"
|
||||
/>
|
||||
<path
|
||||
d="M0 514C147.5 514.333 294.5 513.735 380.5 513.735C405.976 514.94 422.849 515.228 436.37 515.123C477.503 514.803 518.631 506.605 559.508 511.197C564.04 511.706 569.162 512.524 575 513.735C588 516.433 616 521.702 627.5 519.402C647.5 515.402 659 499.235 680.5 499.235C700.5 499.235 725 529.235 742 528.735C757.654 528.735 768.77 510.583 791.793 500.59C798.991 497.465 807.16 496.777 814.423 499.745C832.335 507.064 850.418 524.648 866 524.235C882.791 524.235 902.316 509.786 921.814 505.392Line truncated
|
||||
stroke="#B1C5FF"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
pathLength={1}
|
||||
filter="url(#blurMe)"
|
||||
/>
|
||||
<path
|
||||
d="M0 438.5C150.5 438.5 261 438.318 323.5 456.5C351 464.5 387.517 484.001 423.5 494.5C447.371 501.465 472 503.735 487 507.735C503.786 512.212 504.5 516.808 523 518.735C547 521.235 564.814 501.235 584.5 501.235C604.5 501.235 626 529.069 643 528.569C658.676 528.569 672.076 511.63 695.751 501.972C703.017 499.008 711.231 498.208 718.298 501.617C735.448 509.889 751.454 529.98 767 529.569C783.364 529.569 801.211 507.687 819.903 500.657C825.718 498.469 832.141 499.104 837.992 501.194C859.178Line truncated
|
||||
stroke="#4FABFF"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
pathLength={1}
|
||||
filter="url(#blurMe)"
|
||||
/>
|
||||
<path
|
||||
d="M0.5 364C145.288 362.349 195 361.5 265.5 378C322 391.223 399.182 457.5 411 467.5C424.176 478.649 456.916 491.677 496.259 502.699C498.746 503.396 501.16 504.304 503.511 505.374C517.104 511.558 541.149 520.911 551.5 521.236C571.5 521.236 590 498.736 611.5 498.736C631.5 498.736 652.5 529.236 669.5 528.736C685.171 528.736 697.81 510.924 721.274 501.036C728.505 497.988 736.716 497.231 743.812 500.579C761.362 508.857 778.421 529.148 794 528.736C810.375 528.736 829.35 508.68 848.364 502.1Line truncated
|
||||
stroke="#076EFF"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
pathLength={1}
|
||||
filter="url(#blurMe)"
|
||||
/>
|
||||
|
||||
<defs>
|
||||
<filter id="blurMe">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="5" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Loaded 50 of 61 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user