Compare commits
104
Commits
d68806b2f2
...
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 | ||
|
|
582d1901f6 | ||
|
|
c5b96779aa | ||
|
|
d54aa7e2c2 | ||
|
|
6c6330cd7a | ||
|
|
e288f659c2 | ||
|
|
faebb51d90 | ||
|
|
8093dd2b7f | ||
|
|
046f201e63 |
No files matched your search
@@ -0,0 +1,14 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
build
|
||||||
|
.git
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
coverage
|
||||||
|
test-results
|
||||||
|
playwright-report
|
||||||
|
.planning
|
||||||
|
*.log
|
||||||
|
.husky
|
||||||
|
next-env.d.ts
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Better Auth
|
||||||
|
# Generate with: bunx @better-auth/cli secret
|
||||||
|
BETTER_AUTH_SECRET=
|
||||||
|
# Full absolute URL of the deployed app, e.g. https://example.com
|
||||||
|
BETTER_AUTH_URL=
|
||||||
|
|
||||||
|
# Public auth URL for the browser client (must match BETTER_AUTH_URL in prod)
|
||||||
|
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# Optional: database URL for better-auth (Kysely/Drizzle adapter)
|
||||||
|
# DATABASE_URL=postgres://user:pass@localhost:5432/db
|
||||||
|
|
||||||
|
# Optional: OpenAPI document URL used by openapi-fetch (generate client from it)
|
||||||
|
# OPENAPI_URL=/api/openapi.json
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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 }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
groups:
|
||||||
|
production:
|
||||||
|
dependency-type: "production"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
development:
|
||||||
|
dependency-type: "development"
|
||||||
|
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"
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
name: lint + typecheck + test + build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Set up Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: "1.3.14"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: bun run lint
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: bun run typecheck
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: bun run test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: bun run format:check
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# test tooling
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
test-results
|
||||||
|
playwright-report
|
||||||
|
blob-report
|
||||||
|
.playwright
|
||||||
|
|
||||||
|
# planning files
|
||||||
|
.planning
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
bun run lint-staged
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
build
|
||||||
|
coverage
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
.git
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
|
"tailwindStylesheet": "./app/globals.css"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
|
||||||
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||||
|
|
||||||
|
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||||
|
|
||||||
|
<!-- END:nextjs-agent-rules -->
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
# syntax = docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM oven/bun:1.3.14 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# --- deps ---
|
||||||
|
FROM base AS deps
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# --- builder ---
|
||||||
|
FROM base AS builder
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# --- runner ---
|
||||||
|
FROM base AS runner
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
RUN addgroup --system --gid 1001 nodejs \
|
||||||
|
&& adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
EXPOSE 3000
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -1 +1,136 @@
|
|||||||
# nextjs-template
|
# Next.js Template
|
||||||
|
|
||||||
|
Production-ready Next.js 16 (App Router, Turbopack) starter on Bun, with
|
||||||
|
shadcn/ui Base UI components, TypeScript strict, and a zero-warning
|
||||||
|
lint/typecheck/test/build pipeline.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Framework:** Next.js 16.3 (App Router, Turbopack, standalone output)
|
||||||
|
- **Runtime:** Bun 1.3.14
|
||||||
|
- **UI:** shadcn/ui Base-Nova (Base UI) + Tailwind CSS v4, lucide-react, motion
|
||||||
|
- **Data:** @tanstack/react-query, @tanstack/react-table
|
||||||
|
- **Forms:** react-hook-form + zod (v4) + @hookform/resolvers
|
||||||
|
- **Auth:** better-auth (memory adapter, swap to Drizzle/Postgres when ready)
|
||||||
|
- **API client:** openapi-fetch + openapi-typescript, ky
|
||||||
|
- **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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
cp .env.example .env # optional: set BETTER_AUTH_SECRET / BETTER_AUTH_URL
|
||||||
|
bun run dev # http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Node ≥ 18 (Bun runtime) — see `packageManager` in package.json.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
| Script | Description |
|
||||||
|
| --------------------------------- | ------------------------------------------------------------------- |
|
||||||
|
| `bun run dev` | Start dev server (Turbopack, NODE_ENV pinned to development) |
|
||||||
|
| `bun run build` | Production build (standalone output for Docker) |
|
||||||
|
| `bun run start` | Serve the production build |
|
||||||
|
| `bun run lint` | ESLint (next core-web-vitals + typescript) |
|
||||||
|
| `bun run lint:fix` | ESLint with autofix |
|
||||||
|
| `bun run typecheck` | `tsc --noEmit` (strict + noUnusedLocals/Parameters) |
|
||||||
|
| `bun run test` | Vitest unit tests (jsdom + jest-dom) |
|
||||||
|
| `bun run test:watch` | Vitest watch mode |
|
||||||
|
| `bun run test:coverage` | Vitest with v8 coverage report |
|
||||||
|
| `bun run test:e2e` | Playwright E2E (Chromium, dedicated :3100) |
|
||||||
|
| `bun run format` / `format:check` | Prettier write / check (Tailwind v4 plugin) |
|
||||||
|
| `bun run api:generate` | Regenerate `types/api.ts` from `/api/openapi` (dev server must run) |
|
||||||
|
| `bun run prepare` | Install husky hooks |
|
||||||
|
|
||||||
|
Git hooks (husky + lint-staged) run ESLint + Prettier on staged files.
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
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
|
||||||
|
api/
|
||||||
|
health/ # GET health JSON
|
||||||
|
openapi/ # OpenAPI 3.1 spec for codegen
|
||||||
|
auth/[...all]/ # better-auth handler
|
||||||
|
items/ # GET list / POST create (zod-validated)
|
||||||
|
components/
|
||||||
|
providers.tsx # QueryClient + Theme + Nuqs + Tooltip + Sonner
|
||||||
|
ui/ # shadcn Base-Nova components (CLI-managed)
|
||||||
|
demo/ # form + table wiring examples
|
||||||
|
hooks/ # use-mobile (useSyncExternalStore)
|
||||||
|
lib/
|
||||||
|
auth.ts # better-auth server (memory adapter)
|
||||||
|
auth-client.ts # better-auth react client
|
||||||
|
api.ts # typed openapi-fetch client
|
||||||
|
http.ts # ky wrapper
|
||||||
|
query-client.ts # react-query client (per-request on server)
|
||||||
|
env.ts # zod-validated env (no import-time throw)
|
||||||
|
types/api.ts # generated openapi-typescript types
|
||||||
|
middleware.ts # auth route guard
|
||||||
|
e2e/ # Playwright specs
|
||||||
|
.github/workflows/ci.yml
|
||||||
|
Dockerfile # multi-stage standalone build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
better-auth is wired but intentionally runs on the in-memory adapter so the
|
||||||
|
template builds without a database.
|
||||||
|
|
||||||
|
1. `bunx @better-auth/cli secret` → put the result in `BETTER_AUTH_SECRET`.
|
||||||
|
2. Set `BETTER_AUTH_URL` (+ `NEXT_PUBLIC_BETTER_AUTH_URL` for the client).
|
||||||
|
3. To persist users, swap `memoryAdapter({})` in `lib/auth.ts` for a real
|
||||||
|
adapter (e.g. `drizzleAdapter(db, { provider: "pg" })`) and add the DB URL.
|
||||||
|
|
||||||
|
Routes under `/protected` require a session (see `middleware.ts`).
|
||||||
|
|
||||||
|
## OpenAPI + typed client
|
||||||
|
|
||||||
|
`app/api/openapi` serves an OpenAPI 3.1 document. Regenerate the typed client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run dev & # server must be up
|
||||||
|
bun run api:generate # writes types/api.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use `lib/api.ts` (`createClient<paths>` from openapi-fetch).
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t nextjs-template .
|
||||||
|
docker run -p 3000:3000 nextjs-template
|
||||||
|
```
|
||||||
|
|
||||||
|
Multi-stage standalone image (~50MB runtime); runs as non-root user; requires
|
||||||
|
NODE_ENV=production and the env vars above at runtime.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Private template.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import { toNextJsHandler } from "better-auth/next-js";
|
||||||
|
|
||||||
|
export const { GET, POST } = toNextJsHandler(auth);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
service: "nextjs-template",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { createItemSchema, MOCK_ITEMS, type Item, type CreateItemOutput } from "@/lib/items";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ items: MOCK_ITEMS });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const body = await request.json().catch(() => null);
|
||||||
|
const parsed = createItemSchema.safeParse(body);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Validation failed",
|
||||||
|
details: parsed.error.flatten().fieldErrors,
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const input: CreateItemOutput = parsed.data;
|
||||||
|
const item: Item = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
...input,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// In a real app: persist to DB here. MOCK_ITEMS is module-scoped, so the
|
||||||
|
// created item lives only for the lifetime of the server process.
|
||||||
|
MOCK_ITEMS.unshift(item);
|
||||||
|
|
||||||
|
return NextResponse.json({ item }, { status: 201 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal OpenAPI document describing the template's own API routes.
|
||||||
|
* Kept in sync with app/api/** — regenerate client types with:
|
||||||
|
* bun run api:generate
|
||||||
|
*/
|
||||||
|
export function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
openapi: "3.1.0",
|
||||||
|
info: {
|
||||||
|
title: "Next.js Template API",
|
||||||
|
version: "0.1.0",
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
"/api/health": {
|
||||||
|
get: {
|
||||||
|
operationId: "getHealth",
|
||||||
|
responses: {
|
||||||
|
"200": {
|
||||||
|
description: "Service health",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
ok: { type: "boolean" },
|
||||||
|
service: { type: "string" },
|
||||||
|
timestamp: { type: "string", format: "date-time" },
|
||||||
|
},
|
||||||
|
required: ["ok", "service", "timestamp"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/api/auth/{action}": {
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "action",
|
||||||
|
in: "path",
|
||||||
|
required: true,
|
||||||
|
schema: { type: "string" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
get: {
|
||||||
|
operationId: "authAction",
|
||||||
|
responses: {
|
||||||
|
"200": {
|
||||||
|
description: "Auth route (Better Auth handler)",
|
||||||
|
content: { "application/json": { schema: { type: "object" } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import { CodeBlock } from "@/components/ui/code-block";
|
||||||
|
import { DemoClient } from "@/components/demo/demo-client";
|
||||||
|
|
||||||
|
const SAMPLE_MD = `# Demo Page
|
||||||
|
|
||||||
|
This page shows the core wiring of the template:
|
||||||
|
|
||||||
|
- **Server component** renders this markdown via \`react-markdown\` + \`remark-gfm\`
|
||||||
|
- **Shiki** highlights code blocks server-side
|
||||||
|
- **React Query** fetches \`/api/items\`
|
||||||
|
- **React Hook Form + Zod** validates the "Add item" form
|
||||||
|
- **better-auth** session is read on the server below
|
||||||
|
|
||||||
|
## Code sample
|
||||||
|
|
||||||
|
\`\`\`ts
|
||||||
|
export function getGreeting(name: string): string {
|
||||||
|
return \`Hello, \${name}!\`;
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
> Tip: copy this page as a starting point for real features.
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function DemoPage() {
|
||||||
|
return (
|
||||||
|
<main className="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-8 p-8">
|
||||||
|
<article className="prose prose-sm dark:prose-invert">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
code(props) {
|
||||||
|
const { children, className, ...rest } = props;
|
||||||
|
const match = /language-(\w+)/.exec(className ?? "");
|
||||||
|
if (match) {
|
||||||
|
return (
|
||||||
|
<CodeBlock
|
||||||
|
code={String(children).replace(/\n$/, "")}
|
||||||
|
lang={match[1]}
|
||||||
|
className="my-4 overflow-x-auto rounded-lg border text-sm"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<code className={className} {...rest}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{SAMPLE_MD}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<DemoClient />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function Error({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||||
|
<h1 className="text-2xl font-semibold">Something went wrong</h1>
|
||||||
|
<p className="max-w-md text-center text-sm text-muted-foreground">
|
||||||
|
{error.message || "An unexpected error occurred."}
|
||||||
|
</p>
|
||||||
|
{error.digest ? (
|
||||||
|
<p className="font-mono text-xs text-muted-foreground">Error ID: {error.digest}</p>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => reset()}
|
||||||
|
className="rounded-lg border bg-background px-4 py-2 text-sm font-medium hover:bg-muted"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+130
@@ -0,0 +1,130 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn/tailwind.css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
--font-heading: var(--font-sans);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.87 0 0);
|
||||||
|
--chart-2: oklch(0.556 0 0);
|
||||||
|
--chart-3: oklch(0.439 0 0);
|
||||||
|
--chart-4: oklch(0.371 0 0);
|
||||||
|
--chart-5: oklch(0.269 0 0);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.87 0 0);
|
||||||
|
--chart-2: oklch(0.556 0 0);
|
||||||
|
--chart-3: oklch(0.439 0 0);
|
||||||
|
--chart-4: oklch(0.371 0 0);
|
||||||
|
--chart-5: oklch(0.269 0 0);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import { Providers } from "@/components/providers";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
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 }>) {
|
||||||
|
return (
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
|
<body className="flex min-h-full flex-col">
|
||||||
|
<Providers>{children}</Providers>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default function Loading() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 items-center justify-center p-8" role="status">
|
||||||
|
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||||
|
<span className="size-4 animate-spin rounded-full border-2 border-foreground/20 border-t-foreground" />
|
||||||
|
Loading…
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||||
|
<p className="text-5xl font-bold text-muted-foreground">404</p>
|
||||||
|
<h1 className="text-2xl font-semibold">Page not found</h1>
|
||||||
|
<p className="max-w-md text-center text-sm text-muted-foreground">
|
||||||
|
The page you are looking for does not exist or has been moved.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="rounded-lg border bg-background px-4 py-2 text-sm font-medium hover:bg-muted"
|
||||||
|
>
|
||||||
|
Back to home
|
||||||
|
</Link>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { HomeHero } from "@/components/home-hero";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return <HomeHero />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { headers } from "next/headers";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function ProtectedPage() {
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="mx-auto flex max-w-2xl flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||||
|
<h1 className="text-2xl font-semibold">Protected Page</h1>
|
||||||
|
{session ? (
|
||||||
|
<div className="flex flex-col items-center gap-2 rounded-lg border p-6">
|
||||||
|
<p className="text-lg">
|
||||||
|
Welcome, <span className="font-medium">{session.user.name}</span>!{" "}
|
||||||
|
<span className="text-muted-foreground">({session.user.email})</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Session expires: {new Date(session.session.expiresAt).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground">No session found.</p>
|
||||||
|
)}
|
||||||
|
<Link href="/" className="text-sm text-primary underline-offset-4 hover:underline">
|
||||||
|
← Back home
|
||||||
|
</Link>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { ShowcaseGallery } from "@/components/showcase/gallery";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "UI Gallery — Aceternity + shadcn",
|
||||||
|
description:
|
||||||
|
"Forty-eight Aceternity UI effects vendored into the template — animated cards, beams, backgrounds, pins, marquees and more, wired for the App Router, alongside the full shadcn Base-Nova set.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECTIONS = [
|
||||||
|
"3D Pin / Marquee",
|
||||||
|
"Backgrounds & Beams",
|
||||||
|
"Cards & Hover",
|
||||||
|
"Text Effects",
|
||||||
|
"Inputs & Buttons",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function UiGalleryPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col items-center bg-zinc-50 dark:bg-zinc-950 dark:bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.04),transparent_55%)]">
|
||||||
|
<div className="w-full border-b border-zinc-200 bg-white/80 py-10 backdrop-blur-sm dark:border-zinc-800 dark:bg-zinc-950/80">
|
||||||
|
<div className="mx-auto flex w-full max-w-6xl flex-col gap-3 px-6">
|
||||||
|
<nav className="text-xs font-medium tracking-wide text-zinc-500 dark:text-zinc-400">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="transition-colors hover:text-zinc-900 dark:hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<span className="px-2">/</span>
|
||||||
|
<span className="text-zinc-800 dark:text-zinc-200">UI Gallery</span>
|
||||||
|
</nav>
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight text-balance text-zinc-900 sm:text-4xl dark:text-zinc-50">
|
||||||
|
Aceternity UI gallery
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-2xl text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||||
|
48 Aceternity effects vendored straight from the{" "}
|
||||||
|
<a
|
||||||
|
href="https://ui.aceternity.com/components"
|
||||||
|
className="font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 hover:decoration-zinc-500 dark:text-zinc-200 dark:decoration-zinc-600"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Aceternity registry
|
||||||
|
</a>{" "}
|
||||||
|
and wired for this template. Everything below is live — hover, click and scroll.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 pt-1">
|
||||||
|
{SECTIONS.map((s) => (
|
||||||
|
<a
|
||||||
|
key={s}
|
||||||
|
href={`#${s.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`}
|
||||||
|
className="rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-300 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400 dark:hover:border-zinc-700 dark:hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main className="w-full max-w-6xl flex-1 px-6 py-12">
|
||||||
|
<Suspense
|
||||||
|
fallback={<div className="py-24 text-center text-sm text-zinc-400">Loading gallery…</div>}
|
||||||
|
>
|
||||||
|
<ShowcaseGallery />
|
||||||
|
</Suspense>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "base-nova",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ItemForm } from "@/components/demo/item-form";
|
||||||
|
import { ItemList } from "@/components/demo/item-list";
|
||||||
|
|
||||||
|
export function DemoClient() {
|
||||||
|
return (
|
||||||
|
<section className="flex flex-col gap-6">
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<ItemForm />
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-sm font-semibold">Items (react-query + react-table)</h2>
|
||||||
|
<ItemList />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
const itemSchema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required").max(100),
|
||||||
|
quantity: z.coerce
|
||||||
|
.number({ error: "Quantity must be a number" })
|
||||||
|
.int("Must be a whole number")
|
||||||
|
.min(0, "Minimum 0")
|
||||||
|
.max(10000, "Maximum 10000"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type ItemFormValues = z.input<typeof itemSchema>;
|
||||||
|
type ItemSubmitValues = z.output<typeof itemSchema>;
|
||||||
|
|
||||||
|
export function ItemForm() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<ItemFormValues, unknown, ItemSubmitValues>({
|
||||||
|
resolver: zodResolver(itemSchema),
|
||||||
|
defaultValues: { name: "", quantity: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async (values: ItemSubmitValues) => {
|
||||||
|
const res = await fetch("/api/items", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(values),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => null);
|
||||||
|
throw new Error(err?.error ?? "Failed to create item");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Item created");
|
||||||
|
reset();
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["items"] });
|
||||||
|
},
|
||||||
|
onError: (err: Error) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((values) => mutation.mutate(values))}
|
||||||
|
className="flex flex-col gap-3 rounded-lg border p-4"
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
|
<h2 className="text-sm font-semibold">Add item (RHF + zod + react-query)</h2>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label htmlFor="name" className="text-xs font-medium">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
{...register("name")}
|
||||||
|
className="rounded-md border bg-background px-3 py-1.5 text-sm outline-none focus:border-ring"
|
||||||
|
placeholder="e.g. USB-C cable"
|
||||||
|
/>
|
||||||
|
{errors.name && <span className="text-xs text-destructive">{errors.name.message}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label htmlFor="quantity" className="text-xs font-medium">
|
||||||
|
Quantity
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="quantity"
|
||||||
|
type="number"
|
||||||
|
{...register("quantity")}
|
||||||
|
className="rounded-md border bg-background px-3 py-1.5 text-sm outline-none focus:border-ring"
|
||||||
|
/>
|
||||||
|
{errors.quantity && (
|
||||||
|
<span className="text-xs text-destructive">{errors.quantity.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || mutation.isPending}
|
||||||
|
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{mutation.isPending ? "Creating…" : "Create item"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { flexRender } from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
legacyCreateColumnHelper as createColumnHelper,
|
||||||
|
getCoreRowModel,
|
||||||
|
useLegacyTable,
|
||||||
|
type LegacyColumnDef,
|
||||||
|
} from "@tanstack/react-table/legacy";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import type { Item } from "@/lib/items";
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Item>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: "Name",
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("quantity", {
|
||||||
|
header: "Qty",
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("createdAt", {
|
||||||
|
header: "Created",
|
||||||
|
cell: (info) => format(new Date(info.getValue<string>()), "MMM d, HH:mm"),
|
||||||
|
}),
|
||||||
|
] as LegacyColumnDef<Item>[];
|
||||||
|
|
||||||
|
export function ItemList() {
|
||||||
|
const { data, isLoading, isError, error, refetch } = useQuery({
|
||||||
|
queryKey: ["items"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await fetch("/api/items");
|
||||||
|
if (!res.ok) throw new Error("Failed to load items");
|
||||||
|
const body = await res.json();
|
||||||
|
return body.items as Item[];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = useLegacyTable({
|
||||||
|
data: data ?? [],
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <p className="text-sm text-muted-foreground">Loading items…</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 text-sm text-destructive">
|
||||||
|
<span>{error.message}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
className="rounded-md border px-2 py-1 hover:bg-muted"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-lg border">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-muted/50 text-left">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<th key={header.id} className="px-4 py-2 font-medium">
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<tr key={row.id} className="border-t hover:bg-muted/30">
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td key={cell.id} className="px-4 py-2">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{data?.length === 0 ? (
|
||||||
|
<p className="px-4 py-3 text-sm text-muted-foreground">No items yet.</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { Cover } from "@/components/ui/cover";
|
||||||
|
import { MagneticButton } from "@/components/ui/magnetic-button";
|
||||||
|
import { SparklesCore } from "@/components/ui/sparkles";
|
||||||
|
import { Spotlight } from "@/components/ui/spotlight";
|
||||||
|
|
||||||
|
export function HomeHero() {
|
||||||
|
return (
|
||||||
|
<div className="relative flex w-full flex-1 flex-col items-center justify-center overflow-hidden bg-zinc-50 antialiased dark:bg-zinc-950 dark:bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.06),transparent_60%)]">
|
||||||
|
<div className="pointer-events-none absolute inset-0 z-0">
|
||||||
|
<Spotlight className="-top-40 left-0 md:-top-20 md:left-60" fill="white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sparkles fill the hero background */}
|
||||||
|
<div className="absolute inset-0 z-0 h-full w-full">
|
||||||
|
<SparklesCore
|
||||||
|
id="home-sparkles"
|
||||||
|
background="transparent"
|
||||||
|
minSize={0.6}
|
||||||
|
maxSize={1.4}
|
||||||
|
particleDensity={60}
|
||||||
|
particleColor="#71717a"
|
||||||
|
className="h-full w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.main
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.7, ease: "easeOut" }}
|
||||||
|
className="relative z-10 flex w-full max-w-4xl flex-col items-center gap-8 px-6 py-24 text-center"
|
||||||
|
>
|
||||||
|
<span className="rounded-full border border-zinc-200 bg-white/70 px-3 py-1 text-xs font-medium tracking-wide text-zinc-500 backdrop-blur dark:border-zinc-800 dark:bg-zinc-900/70 dark:text-zinc-400">
|
||||||
|
Next.js 16 · Tailwind v4 · motion · 48 Aceternity effects + shadcn
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h1 className="max-w-3xl text-4xl leading-tight font-semibold tracking-tight text-zinc-900 sm:text-6xl dark:text-white">
|
||||||
|
A template that ships with <Cover>animated UI out of the box.</Cover>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="max-w-xl text-base leading-7 text-zinc-600 dark:text-zinc-300">
|
||||||
|
Aceternity components vendored as source, Tailwind v4 native, zero-warning lint. Every
|
||||||
|
effect below is live — build a landing page without starting from scratch.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-4">
|
||||||
|
<Link href="/ui" passHref>
|
||||||
|
<MagneticButton strength={0.5} maxDistance={60}>
|
||||||
|
<span className="inline-flex items-center gap-2 rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200">
|
||||||
|
Browse the gallery
|
||||||
|
<span aria-hidden>→</span>
|
||||||
|
</span>
|
||||||
|
</MagneticButton>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/demo"
|
||||||
|
className="inline-flex items-center gap-2 rounded-full border border-zinc-300 px-6 py-3 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-900"
|
||||||
|
>
|
||||||
|
View the demo wiring
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</motion.main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { ThemeProvider } from "next-themes";
|
||||||
|
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||||
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import { getQueryClient } from "@/lib/query-client";
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
const queryClient = React.useMemo(() => getQueryClient(), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||||
|
<NuqsAdapter>
|
||||||
|
<TooltipProvider delay={0}>
|
||||||
|
{children}
|
||||||
|
<Toaster richColors closeButton position="bottom-right" />
|
||||||
|
</TooltipProvider>
|
||||||
|
</NuqsAdapter>
|
||||||
|
</ThemeProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,69 @@
|
|||||||
|
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
|
||||||
|
import { cn } from "cn";
|
||||||
|
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||||
|
|
||||||
|
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Root
|
||||||
|
data-slot="accordion"
|
||||||
|
className={cn("flex w-full flex-col", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn("not-last:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:texLine truncated
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon
|
||||||
|
data-slot="accordion-trigger-icon"
|
||||||
|
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
|
||||||
|
/>
|
||||||
|
<ChevronUpIcon
|
||||||
|
data-slot="accordion-trigger-icon"
|
||||||
|
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
|
||||||
|
/>
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Panel
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-(--accordion-panel-height) pt-0 pb-2.5 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</AccordionPrimitive.Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
|
||||||
|
import { cn } from "cn";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
|
||||||
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
|
||||||
|
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
|
||||||
|
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Backdrop
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogContent({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: AlertDialogPrimitive.Popup.Props & {
|
||||||
|
size?: "default" | "sm";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Popup
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
className={cn(
|
||||||
|
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-media"
|
||||||
|
className={cn(
|
||||||
|
"mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
data-slot="alert-dialog-title"
|
||||||
|
className={cn(
|
||||||
|
"font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
data-slot="alert-dialog-description"
|
||||||
|
className={cn(
|
||||||
|
"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof Button>) {
|
||||||
|
return <Button data-slot="alert-dialog-action" className={cn(className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogCancel({
|
||||||
|
className,
|
||||||
|
variant = "outline",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: AlertDialogPrimitive.Close.Props &
|
||||||
|
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Close
|
||||||
|
data-slot="alert-dialog-cancel"
|
||||||
|
className={cn(className)}
|
||||||
|
render={<Button variant={variant} size={size} />}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogMedia,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { cn } from "cn";
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div data-slot="alert-action" className={cn("absolute top-2 right-2", className)} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription, AlertAction };
|
||||||
@@ -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,22 @@
|
|||||||
|
import { cn } from "cn";
|
||||||
|
|
||||||
|
function AspectRatio({
|
||||||
|
ratio,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { ratio: number }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="aspect-ratio"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--ratio": ratio,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cn("relative aspect-(--ratio)", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AspectRatio };
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { mergeProps } from "@base-ui/react/merge-props";
|
||||||
|
import { useRender } from "@base-ui/react/use-render";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { cn } from "cn";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const attachmentVariants = cva(
|
||||||
|
"group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
default:
|
||||||
|
"gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2",
|
||||||
|
sm: "gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5",
|
||||||
|
xs: "gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1",
|
||||||
|
},
|
||||||
|
orientation: {
|
||||||
|
horizontal: "min-w-40 items-center",
|
||||||
|
vertical: "w-24 flex-col has-data-[slot=attachment-content]:w-30",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function Attachment({
|
||||||
|
className,
|
||||||
|
state = "done",
|
||||||
|
size = "default",
|
||||||
|
orientation = "horizontal",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> &
|
||||||
|
VariantProps<typeof attachmentVariants> & {
|
||||||
|
state?: "idle" | "uploading" | "processing" | "error" | "done";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="attachment"
|
||||||
|
data-state={state}
|
||||||
|
data-size={size}
|
||||||
|
data-orientation={orientation}
|
||||||
|
className={cn(attachmentVariants({ size, orientation }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachmentMediaVariants = cva(
|
||||||
|
"relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-noLine truncated
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
icon: "",
|
||||||
|
image:
|
||||||
|
"opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "icon",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function AttachmentMedia({
|
||||||
|
className,
|
||||||
|
variant = "icon",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof attachmentMediaVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="attachment-media"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(attachmentMediaVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="attachment-content"
|
||||||
|
className={cn(
|
||||||
|
"max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentTitle({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="attachment-title"
|
||||||
|
className={cn(
|
||||||
|
"block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentDescription({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="attachment-description"
|
||||||
|
className={cn(
|
||||||
|
"mt-0.5 block min-w-0 truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive/80",
|
||||||
|
"max-w-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentActions({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="attachment-actions"
|
||||||
|
className={cn(
|
||||||
|
"relative z-20 flex shrink-0 items-center group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:right-3 group-data-[orientation=vertical]/attachment:gap-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentAction({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size = "icon-xs",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-slot="attachment-action"
|
||||||
|
variant={variant ?? "ghost"}
|
||||||
|
size={size}
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentTrigger({
|
||||||
|
className,
|
||||||
|
render,
|
||||||
|
type,
|
||||||
|
...props
|
||||||
|
}: useRender.ComponentProps<"button">) {
|
||||||
|
return useRender({
|
||||||
|
defaultTagName: "button",
|
||||||
|
props: mergeProps<"button">(
|
||||||
|
{
|
||||||
|
type: render ? type : (type ?? "button"),
|
||||||
|
className: cn("absolute inset-0 z-10 outline-none", className),
|
||||||
|
},
|
||||||
|
props
|
||||||
|
),
|
||||||
|
render,
|
||||||
|
state: {
|
||||||
|
slot: "attachment-trigger",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="attachment-group"
|
||||||
|
className={cn(
|
||||||
|
"flex min-w-0 scroll-fade-x snap-x snap-mandatory scroll-px-1 scrollbar-none gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Attachment,
|
||||||
|
AttachmentGroup,
|
||||||
|
AttachmentMedia,
|
||||||
|
AttachmentContent,
|
||||||
|
AttachmentTitle,
|
||||||
|
AttachmentDescription,
|
||||||
|
AttachmentActions,
|
||||||
|
AttachmentAction,
|
||||||
|
AttachmentTrigger,
|
||||||
|
};
|
||||||
@@ -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,92 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
|
||||||
|
import { cn } from "cn";
|
||||||
|
|
||||||
|
function Avatar({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: AvatarPrimitive.Root.Props & {
|
||||||
|
size?: "default" | "sm" | "lg";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn("aspect-square size-full rounded-full object-cover", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="avatar-badge"
|
||||||
|
className={cn(
|
||||||
|
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
|
||||||
|
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||||
|
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||||
|
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="avatar-group"
|
||||||
|
className={cn(
|
||||||
|
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="avatar-group-count"
|
||||||
|
className={cn(
|
||||||
|
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge };
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loaded 50 of 173 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user