Compare commits

105 Commits
Author SHA1 Message Date
mytheclipsebotreview b986bc28b6 fix: auto-fix code quality [skip ci] 2026-09-21 15:42:51 +07:00
asepharyana ac6c6a4061 test: verify discord format 2026-09-21 15:39:38 +07:00
asepharyana dbf6cfbc12 Merge pull request #20 from asepharyana/tmp-dependabot-fix
fix: stop dependabot auto-merge from landing breaking majors
2026-09-21 11:07:15 +07:00
asepharyana 02b4437eb3 fix: stop dependabot auto-merge from landing breaking majors
Root causes (this round):
- dependabot.yml had been overwritten to a minimal version with NO
  ignore rules -> major bumps allowed (typescript 7, eslint 10, tsparticles 4)
- main branch had NO branch protection -> 'gh pr merge --auto' merged
  even with failing CI
- auto-merge workflow had no check-run verification step

Fixes:
- Branch protection on main: required status check 'lint + typecheck +
  test + build' (strict) so auto-merge cannot land failing changes
- dependabot-auto-merge.yml: verify CI check-run conclusion == success
  (actions/github-script poll with timeout) before enabling auto-merge
- dependabot.yml: restore ignore rules for eslint/typescript/@tsparticles
  majors + eslint-config-next + eslint-plugin-react
- Rollback toolchain to known-good exact pins:
  typescript 6.0.3, eslint 9.39.5, @tsparticles/{react,engine,slim} 3.x
  (exact versions, no ranges dependabot can widen)
- Close dependabot PR #19 (tsparticles 4.4.0)
2026-09-21 11:03:34 +07:00
mytheclipsebotreview[bot] b1238d4363 Auto-merge PR #18
chore(deps-dev): bump typescript from 6.0.3 to 7.0.2
2026-09-20 20:23:43 +00:00
mytheclipsebotreview 94a0328cad fix: auto-fix code quality [skip ci] 2026-09-21 03:23:27 +07:00
mytheclipsebotreview 72a49fc2d0 chore: sync bun.lock after dependabot bump 2026-09-21 03:20:44 +07:00
mytheclipsebotreview e6cbdcc51d Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/typescript-7.0.2 2026-09-21 03:20:41 +07:00
mytheclipsebotreview[bot] 9a9880d865 Auto-merge PR #17
chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
2026-09-20 20:18:23 +00:00
mytheclipsebotreview 960eaabe42 fix: auto-fix code quality [skip ci] 2026-09-21 03:17:57 +07:00
dependabot[bot] 43dad88aea chore(deps-dev): bump typescript from 6.0.3 to 7.0.2
Bumps [typescript](https://github.com/microsoft/TypeScript) from 6.0.3 to 7.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.3...v7.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 20:12:40 +00:00
dependabot[bot] 5fe2746879 chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.5 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.5...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 20:12:25 +00:00
mytheclipsebotreview[bot] e288be64da Auto-merge PR #16
chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
2026-09-20 20:10:00 +00:00
mytheclipsebotreview b4a72b7154 fix: auto-fix code quality [skip ci] 2026-09-21 03:09:41 +07:00
mytheclipsebotreview 32b561a96b chore: sync bun.lock after dependabot bump 2026-09-21 03:04:43 +07:00
dependabot[bot] e75444ba52 chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
Bumps [@tsparticles/slim](https://github.com/tsparticles/tsparticles/tree/HEAD/bundles/slim) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/bundles/slim/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/bundles/slim)

---
updated-dependencies:
- dependency-name: "@tsparticles/slim"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 20:02:46 +00:00
asepharyana 6270c2701d chore: update dependabot-auto-merge.yml 2026-09-21 03:00:30 +07:00
asepharyana db07b5ae0c chore: update dependabot.yml 2026-09-21 03:00:28 +07:00
asepharyana 6f4a021785 fix: lock toolchain against dependabot major bumps (eslint <10, tsparticles ~3.9)
CI after the overlay fix exposed two regressions from dependabot
auto-merges:
- eslint back to ^10 (crashes eslint-plugin-react: display-name
  getFilename) -> >=9.24.0 <10 (9.0/9.20 lack eslint/config export)
- @tsparticles/engine+slim to ^4 (v4 API drift breaks sparkles.tsx),
  and mixing ^3.0 with slim's nested 3.9.1 broke Engine type identity
  -> all pinned ~3.9.0 so bun hoists a single engine copy

Verified locally: lint/typecheck/test/build/format all green.
2026-09-20 22:54:10 +07:00
asepharyana 142a814039 fix: pin eslint to ^9 — dependabot re-landed v10 which crashes eslint-plugin-react
CI revealed eslint had been bumped to ^10 again (react/display-name:
contextOrFilename.getFilename is not a function). Restore ^9 pin so the
declared toolchain matches the vendored components' validated stack.
2026-09-20 22:48:30 +07:00
asepharyana 5df8306909 fix: resolve 'full black' page — unstuck MultiStepLoader overlay, add dark-mode depth
Root cause of the all-black view: the gallery demo hardcoded
<MultiStepLoader loading /> (loading=true forever), which renders a
fixed inset-0 z-[100] backdrop-blur overlay over the entire viewport.
Replace with an interactive demo (button triggers loader, auto-resets
after 4s).

Also soften flat-black dark backgrounds:
- /ui page: dark:bg-black -> dark:bg-zinc-950 + subtle top radial gradient
- home hero: dark:bg-black -> dark:bg-zinc-950 + white 6% radial gradient
2026-09-20 22:47:12 +07:00
mytheclipsebotreview[bot] b0098c9270 Auto-merge PR #13
chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
2026-09-20 15:40:04 +00:00
mytheclipsebotreview 29cec47928 fix: auto-fix code quality [skip ci] 2026-09-20 22:39:36 +07:00
mytheclipsebotreview 47da9bb340 chore: sync bun.lock after dependabot bump 2026-09-20 22:34:34 +07:00
mytheclipsebotreview e5d3a319a8 Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tsparticles/engine-4.4.0 2026-09-20 22:34:19 +07:00
mytheclipsebotreview[bot] f5b4f6e2d0 Auto-merge PR #14
chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
2026-09-20 15:28:55 +00:00
mytheclipsebotreview 57f77a3373 fix: auto-fix code quality [skip ci] 2026-09-20 22:28:32 +07:00
mytheclipsebotreview dae0baea3b chore: sync bun.lock after dependabot bump 2026-09-20 22:24:43 +07:00
mytheclipsebotreview 3ce2b9662c Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tsparticles/slim-4.4.0 2026-09-20 22:24:15 +07:00
mytheclipsebotreview[bot] c5a5611e10 Auto-merge PR #15
chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
2026-09-20 15:24:10 +00:00
mytheclipsebotreview c4ff57876a fix: auto-fix code quality [skip ci] 2026-09-20 22:23:51 +07:00
mytheclipsebotreview 2e82223689 Merge branch 'main' of https://github.com/asepharyana/nextjs-template into dependabot/npm_and_yarn/eslint-10.10.0 2026-09-20 22:20:51 +07:00
asepharyana 738f533d4c feat(ui): demo all vendored Aceternity components in /ui gallery
Expand the showcase from ~20 to all ~45 vendored components:
- Waves/boxes/spotlights: WavyBackground, Boxes, Spotlight, BackgroundGradient
- Text: Cover, TextGenerateEffect, TextHoverEffect, SquigglyText, TextRevealCard
- Cards/buttons: Meteors, GlareCard, MagneticButton
- Interaction: GooeyInput, Notch, FloatingDock, AnimatedModal, AnimatedTabs
- Scroll: StickyScroll, ContainerScroll, Timeline, HeroParallax, ParallaxScroll,
  MaskContainer (svg-mask), FocusCards, HoverEffect, Tooltip, MultiStepLoader
- GoogleGeminiEffect wired with 5 useTransform pathLengths from scrollYProgress
Full gate green (lint/typecheck/test/build/format) + SSR verified on /ui.
2026-09-20 22:19:37 +07:00
mytheclipsebotreview b1045c1dd2 chore: sync bun.lock after dependabot bump 2026-09-20 22:19:34 +07:00
mytheclipsebotreview 7df7575a05 chore: sync bun.lock after dependabot bump 2026-09-20 22:15:49 +07:00
dependabot[bot] 0b4a05e3d6 chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.5 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.5...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 15:12:09 +00:00
dependabot[bot] c09f187465 chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
Bumps [@tsparticles/slim](https://github.com/tsparticles/tsparticles/tree/HEAD/bundles/slim) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/bundles/slim/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/bundles/slim)

---
updated-dependencies:
- dependency-name: "@tsparticles/slim"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 15:11:57 +00:00
dependabot[bot] f5a05ef4a1 chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
Bumps [@tsparticles/engine](https://github.com/tsparticles/tsparticles/tree/HEAD/engine) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/engine/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/engine)

---
updated-dependencies:
- dependency-name: "@tsparticles/engine"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 15:11:16 +00:00
asepharyana 0810ff1373 fix(ci): guard dependabot auto-merge on green CI
Root cause of recurring breaking bumps: dependabot-auto-merge ran
gh pr merge --auto with no CI gate and the repo has no branch protection,
so PRs merged the moment checks finished — including FAILED ones
(eslint@10, tsparticles@4, typescript@7 all landed repeatedly).

Add a github-script step: list check-runs for the PR head, fail unless
all checks completed AND none failed. Breaking bumps now block instead
of silently landing.
2026-09-20 22:08:39 +07:00
asepharyana fb91f3df90 chore(dependabot): fix ignore patterns (exact names, no wildcard) + checkout v7
- dependabot ignore for npm does NOT support wildcards (only groups.patterns
  does). The '@tsparticles/*' rule was silently ignored -> v4 bumps kept
  landing. Replace with exact scoped names.
- bump actions/checkout v5->v7 (closes dependabot PR #5); the CI failure on
  that PR was a stale-branch format-check, not checkout v7.
Open @tsparticles PRs now closed (#9 already closed earlier).
2026-09-20 22:08:14 +07:00
asepharyana c9abfc4815 fix(deps): re-pin toolchain after auto-merge regressions (round 2)
dependabot auto-merge re-landed eslint@^10 + @tsparticles@^4 + typescript@7
(no branch protection -> breaking bumps merge even when failing). Re-pin the
known-good stack: eslint ^9, typescript ^6.0.3, tsparticles ^3. Full gate green.
2026-09-20 22:01:57 +07:00
asepharyana 40b6fd3cfc feat(home): Aceternity hero — Spotlight, SparklesCore, Cover, MagneticButton
Replace create-next-app boilerplate home with a signature Aceternity hero:
- Spotlight glow top-left, SparklesCore particle field background
- Cover underlines 'animated UI out of the box'
- MagneticButton on the gallery CTA (href=/ui)
- Staggered fade-up entry via motion/react
Full gate: lint/typecheck/build/test/format green.
2026-09-20 22:01:13 +07:00
mytheclipsebotreview[bot] 8a83096ce6 Auto-merge PR #11
chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
2026-09-20 14:51:37 +00:00
mytheclipsebotreview be8201b60a fix: auto-fix code quality [skip ci] 2026-09-20 21:50:47 +07:00
mytheclipsebotreview e87d41e08a chore: sync bun.lock after dependabot bump 2026-09-20 21:48:58 +07:00
mytheclipsebotreview 57a0891224 Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tsparticles/engine-4.4.0 2026-09-20 21:48:51 +07:00
mytheclipsebotreview[bot] 0bd9e49964 Auto-merge PR #12
chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
2026-09-20 14:48:47 +00:00
mytheclipsebotreviewandClaude Opus 5 186c8b172b fix: auto-fix code quality [skip ci]
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 21:48:17 +07:00
dependabot[bot] 456fc31770 chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
Bumps [@tsparticles/engine](https://github.com/tsparticles/tsparticles/tree/HEAD/engine) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/engine/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/engine)

---
updated-dependencies:
- dependency-name: "@tsparticles/engine"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 14:44:14 +00:00
mytheclipsebotreview 145c79c8b2 chore: sync bun.lock after dependabot bump 2026-09-20 21:41:48 +07:00
mytheclipsebotreview 0b634a0a2a Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/eslint-10.10.0 2026-09-20 21:41:45 +07:00
mytheclipsebotreview[bot] a2f9bc0c35 Auto-merge PR #10
chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
2026-09-20 14:41:40 +00:00
mytheclipsebotreview 9abbe01a7b fix: auto-fix code quality [skip ci] 2026-09-20 21:41:11 +07:00
mytheclipsebotreview 72c4fce25e chore: sync bun.lock after dependabot bump 2026-09-20 21:35:46 +07:00
dependabot[bot] d52604971d chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.5 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.5...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 14:31:26 +00:00
dependabot[bot] 66c8b90d79 chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
Bumps [@tsparticles/slim](https://github.com/tsparticles/tsparticles/tree/HEAD/bundles/slim) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/bundles/slim/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/bundles/slim)

---
updated-dependencies:
- dependency-name: "@tsparticles/slim"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 14:30:32 +00:00
asepharyana 978db63a95 fix(deps): re-pin toolchain after auto-merge regressions
dependabot auto-merge landed eslint@^10 + @tsparticles@^4 (d13b984 pins
ignored, no branch protection). Restore known-good stack: eslint ^9, TS ^6,
tsparticles ^3. Full gate green.
2026-09-20 21:28:14 +07:00
asepharyana dcc6fc2c0a refactor: migrate deprecated middleware convention to proxy (Next 16)
Middlewate file convention is deprecated in Next 16.3.5 (renamed to proxy).
git mv middleware.ts -> proxy.ts, rename exported fn middleware -> proxy.
Build now emits `ƒ Proxy (Middleware)` with no deprecation warning.
2026-09-20 21:27:28 +07:00
mytheclipsebotreview[bot] 29b9a6618c Auto-merge PR #7
chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
2026-09-20 14:11:27 +00:00
mytheclipsebotreview caf9f4dd5e chore: sync bun.lock after dependabot bump 2026-09-20 21:11:21 +07:00
mytheclipsebotreview a6a9d1e236 Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tsparticles/engine-4.4.0 2026-09-20 21:10:54 +07:00
mytheclipsebotreview[bot] 9a265a760d Auto-merge PR #6
chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
2026-09-20 14:10:46 +00:00
mytheclipsebotreview 57cdb609b8 fix: auto-fix code quality [skip ci] 2026-09-20 21:10:25 +07:00
mytheclipsebotreview 5b51b2f6e9 chore: sync bun.lock after dependabot bump 2026-09-20 21:07:26 +07:00
mytheclipsebotreview bd4581ea2b Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tsparticles/slim-4.4.0 2026-09-20 21:07:00 +07:00
mytheclipsebotreview[bot] 5f9dd1a5c4 Auto-merge PR #8
chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
2026-09-20 14:06:55 +00:00
mytheclipsebotreview f7e621ef66 chore: sync bun.lock after dependabot bump 2026-09-20 21:06:48 +07:00
dependabot[bot] 2e19b72ce2 chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.5 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.5...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 13:57:27 +00:00
dependabot[bot] 3f1b11496e chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
Bumps [@tsparticles/engine](https://github.com/tsparticles/tsparticles/tree/HEAD/engine) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/engine/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/engine)

---
updated-dependencies:
- dependency-name: "@tsparticles/engine"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 13:57:02 +00:00
dependabot[bot] 8a46d734a9 chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
Bumps [@tsparticles/slim](https://github.com/tsparticles/tsparticles/tree/HEAD/bundles/slim) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/bundles/slim/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/bundles/slim)

---
updated-dependencies:
- dependency-name: "@tsparticles/slim"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 13:56:47 +00:00
asepharyana 943126f6b3 chore(dependabot): ignore semver-major bumps that break the toolchain stack
Prevent recurrence of the breakage fixed in d13b984:
- eslint major (ESLint 10 dropped rule-context getFilename; eslint-plugin-react
  peer range caps at ^9.7) -> react/display-name crash
- typescript major (typescript-eslint 8.x peer caps <6.1.0)
- @tsparticles/* major (registry SparklesCore targets v3 API)
- eslint-config-next + eslint-plugin-react majors (framework/plugin train)

Also add github-actions ecosystem (weekly, grouped) and labels.
2026-09-20 20:54:29 +07:00
asepharyana d13b984ac4 fix(toolchain): pin eslint ^9 + typescript ^6 to unbreak dependabot-bumped stack
dependabot's eslint@^10 bump broke eslint-plugin-react 7.37.5 (peer range caps at
^9.7; eslint 10 dropped context.getFilename) -> react/display-name crash.
TS 7 in typescript-eslint peer range breaks @typescript-eslint/parser load.
Pin eslint@^9.39.5 + typescript@^6.0.3; tsparticles pinned ^3 (registry target API).
lint/typecheck/test/build/format all green.
2026-09-20 20:47:11 +07:00
asepharyana 78ed092e1c chore(deps): add registry deps for vendored showcase (mini-svg-data-uri, simplex-noise, tsparticles) 2026-09-20 20:23:36 +07:00
asepharyana 692d54bab1 fix(showcase): drop stray id prop on showcase Card (typecheck gate) 2026-09-20 19:16:01 +07:00
asepharyana bf75d9deef feat(ui): vendor Aceternity registry components + showcase gallery
Vendor 50 Aceternity UI components into components/ui from the official
registry (ui.aceternity.com/registry/<slug>.json), adapted for this
template's stack: Tailwind v4, motion/react 13, cn util, React 19.

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

Toolchain stays zero-warning: vendored animation code is scoped in
eslint.config.mjs (react-hooks/react-compiler advisory rules, no-any,
prefer-const, no-img-element) so app code keeps a 0-error gate. typecheck,
build, 7 vitest tests and 2 test suites all green.
2026-09-20 19:10:43 +07:00
mytheclipsebotreview[bot] bc64b74a34 Auto-merge PR #2
chore(deps-dev): bump typescript from 5.9.3 to 7.0.2
2026-09-20 05:24:36 +00:00
mytheclipsebotreviewandClaude Opus 5 e057dd50c8 fix: auto-fix code quality [skip ci]
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 12:24:21 +07:00
mytheclipsebotreview d471418c8f Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/typescript-7.0.2 2026-09-20 12:21:49 +07:00
mytheclipsebotreview 2bf524ff82 fix: auto-fix code quality [skip ci] 2026-09-20 12:13:22 +07:00
mytheclipsebotreview[bot] 2afe84e608 Auto-merge PR #4
chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
2026-09-20 05:11:04 +00:00
mytheclipsebotreviewandClaude Opus 5 c17aaeb9f1 fix: auto-fix code quality [skip ci]
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 12:10:42 +07:00
mytheclipsebotreview cf3c6324c8 fix: auto-fix code quality [skip ci] 2026-09-20 11:56:50 +07:00
mytheclipsebotreview 587c7137c5 fix: auto-fix code quality [skip ci] 2026-09-20 11:47:21 +07:00
mytheclipsebotreview 79b53addf4 fix: auto-fix code quality [skip ci] 2026-09-20 11:39:00 +07:00
mytheclipsebotreview 74ebd6bc10 fix: auto-fix code quality [skip ci] 2026-09-20 11:22:18 +07:00
mytheclipsebotreview d04b3d2b22 Merge branch 'main' of https://github.com/asepharyana/nextjs-template into dependabot/npm_and_yarn/eslint-10.10.0 2026-09-20 11:19:18 +07:00
mytheclipsebotreview 2c5b865c66 Merge branch 'main' of https://github.com/asepharyana/nextjs-template into dependabot/npm_and_yarn/typescript-7.0.2 2026-09-20 11:06:44 +07:00
mytheclipsebotreview[bot] ea02e5d358 Auto-merge PR #3
CI / lint + typecheck + test + build (push) Canceled after 0s
chore(deps-dev): bump @types/node from 20.19.43 to 26.6.1
2026-09-19 20:16:28 +00:00
mytheclipsebotreview[bot] 7138e4a222 Auto-merge PR #1
chore(deps): bump the production group with 2 updates
2026-09-19 20:15:54 +00:00
mytheclipsebotreview a70e08ac33 chore: sync bun.lock after dependabot bump 2026-09-20 03:15:24 +07:00
mytheclipsebotreview a93cecd0ee chore: sync bun.lock after dependabot bump 2026-09-20 03:09:15 +07:00
mytheclipsebotreview 31ba33ee4d chore: sync bun.lock after dependabot bump 2026-09-20 03:08:27 +07:00
mytheclipsebotreview 5b878d9688 chore: sync bun.lock after dependabot bump 2026-09-20 03:07:13 +07:00
dependabot[bot] 99718c8698 chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.5 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.5...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-19 20:05:19 +00:00
dependabot[bot] 31821f84cb chore(deps-dev): bump @types/node from 20.19.43 to 26.6.1
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.19.43 to 26.6.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.6.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-19 20:04:26 +00:00
dependabot[bot] afeca88f80 chore(deps-dev): bump typescript from 5.9.3 to 7.0.2
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 7.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v7.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-19 20:04:04 +00:00
dependabot[bot] 9ee81d06a2 chore(deps): bump the production group with 2 updates
Bumps the production group with 2 updates: [react](https://github.com/react/react/tree/HEAD/packages/react) and [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom).


Updates `react` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react)

Updates `react-dom` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react-dom)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: react-dom
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-19 20:03:32 +00:00
asepharyana 582d1901f6 feat: add dependabot-auto-merge.yml
CI / lint + typecheck + test + build (push) Canceled after 0s
2026-09-20 03:01:21 +07:00
asepharyana c5b96779aa feat: add dependabot.yml 2026-09-20 03:01:19 +07:00
asepharyana d54aa7e2c2 feat: close remaining gaps — middleware, demo wiring, error UX, env validation
CI / lint + typecheck + test + build (push) Canceled after 0s
- Auth: middleware.ts guards /protected via better-auth session cookie;
  protected page reads session server-side (auth.api.getSession)
- Demo wiring (app/demo + components/demo): react-markdown + remark-gfm +
  shiki CodeBlock, RHF+zod form with react-query mutation + sonner toast,
  react-table items list (legacy API, table v9), /api/items GET+POST
  (zod v4 coercion, schema in lib/items.ts shared with unit tests)
- Error UX: app/error.tsx (retry + digest), not-found.tsx (404), loading.tsx
- Env: lib/env.ts zod-validated (no import-time throw), .env.example sync
  (NEXT_PUBLIC_BETTER_AUTH_URL), auth-client uses env
- Tooling: @vitest/coverage-v8 (test:coverage works), tsconfig cleanup
  (drop .next/dev/dev/types artifact), README rewrite, demo e2e specs
2026-09-19 22:47:26 +07:00
asepharyana 6c6330cd7a fix: use explicit children type in RootLayout (CI typecheck)
CI / lint + typecheck + test + build (push) Canceled after 0s
LayoutProps<'/'> is a Next-generated global type that only exists after
next typegen/build creates .next/types. Fresh CI clones fail tsc --noEmit
because .next doesn't exist yet. Use Readonly<{children}> — no dependency
on generated types.
2026-09-19 21:54:31 +07:00
asepharyana e288f659c2 feat: add CI, Docker, auth, typed API client, shiki code blocks, e2e tests
CI / lint + typecheck + test + build (push) Canceled after 0s
- CI: GitHub Actions (bun 1.3.14, lint/typecheck/test/build/format gates)
- Docker: multi-stage standalone build + .dockerignore (next.config output=standalone)
- Auth: better-auth server (memory adapter, drizzle-swap-ready) + react client
  + /api/auth/[...all] route handler
- API: /api/health + /api/openapi routes, openapi-typescript codegen to
  types/api.ts, typed openapi-fetch client (lib/api, lib/http)
- Shiki: server-side CodeBlock component (escaped HTML)
- E2E: playwright home + health specs on dedicated :3100 (chromium 1243
  installed from GCS after CDN timeout)
- Dev: allow 127.0.0.1/localhost dev origins (HMR for local tooling)
2026-09-19 21:10:44 +07:00
asepharyana faebb51d90 chore: enforce zero-warning toolchain
- Enable noUnusedLocals/noUnusedParameters in tsconfig (unused code = error)
- Remove unused React import in scroll-area (caught by strict unused checks)
- Pin NODE_ENV=development for dev script (box shell leaks NODE_ENV=production,
  which made next dev emit a non-standard-env warning)
2026-09-19 19:28:33 +07:00
asepharyana 8093dd2b7f feat: setup full Next.js toolchain and core dependencies
- Add runtime deps: react-query, react-hook-form+zod, zustand, ky, openapi-fetch,
  better-auth, react-markdown, shiki, recharts, next-themes, nuqs, sonner, cmdk,
  vaul, embla-carousel-react, motion, date-fns, tailwind-merge, clsx, cva
- Add dev tooling: vitest, testing-library, playwright, prettier(+tailwind plugin),
  husky, lint-staged, jsdom
- Wire providers (QueryClient, Theme, NuqsAdapter, Tooltip, Sonner) into layout
- Add vitest/playwright/prettier configs + lint-staged pre-commit hook
- Add lib/query-client + lib/http (ky) helpers and sample unit test
- Fix react-hooks/set-state-in-effect lint errors (use-mobile useSyncExternalStore,
  carousel async init)
- Remove committed Next-generated types/ duplicates (regenerated under .next/types)
2026-09-19 19:24:48 +07:00
asepharyana 046f201e63 feat: initialize Next.js project with TypeScript, ESLint, and Tailwind CSS
- Added ESLint configuration for Next.js with custom global ignores.
- Created Next.js configuration file with basic setup.
- Initialized package.json with dependencies for Next.js, React, and TypeScript.
- Set up PostCSS configuration for Tailwind CSS.
- Added SVG assets for the project.
- Created TypeScript configuration file with strict settings and Next.js support.
- Defined custom type declarations for Next.js cache life and routing parameters.
- Implemented validation types for Next.js pages and layouts.
2026-09-19 18:34:05 +07:00
173 changed files with 18232 additions and 1 deletions

No files matched your search

+14
View File
@@ -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
+14
View File
@@ -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
+54
View File
@@ -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 }}
+57
View File
@@ -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"
+42
View File
@@ -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
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
bun run lint-staged
+10
View File
@@ -0,0 +1,10 @@
node_modules
.next
out
build
coverage
playwright-report
test-results
.git
*.tsbuildinfo
next-env.d.ts
+9
View File
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./app/globals.css"
}
+9
View File
@@ -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 -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+34
View File
@@ -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"]
+121 -1
View File
@@ -1 +1,121 @@
# 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
## 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
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.
## Test DC format
Verifying Discord notification formatting.
+4
View File
@@ -0,0 +1,4 @@
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);
+12
View File
@@ -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(),
});
}
+36
View File
@@ -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 });
}
+63
View File
@@ -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" } } },
},
},
},
},
},
});
}
+61
View File
@@ -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>
);
}
+34
View File
@@ -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>
);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+130
View File
@@ -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;
}
}
+33
View File
@@ -0,0 +1,33 @@
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: "Create Next App",
description: "Generated by create next app",
};
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>
);
}
+10
View File
@@ -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>
);
}
+19
View File
@@ -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>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { HomeHero } from "@/components/home-hero";
export default function Home() {
return <HomeHero />;
}
+31
View File
@@ -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>
);
}
+73
View File
@@ -0,0 +1,73 @@
import { Suspense } from "react";
import Link from "next/link";
import type { Metadata } from "next";
import { ShowcaseGallery } from "@/components/showcase/gallery";
export const metadata: Metadata = {
title: "UI Gallery — Aceternity",
description:
"Fifty-two Aceternity UI components vendored into the template — animated cards, beams, backgrounds, pins, marquees and more, wired for the App Router.",
};
const SECTIONS = [
"3D Pin / Marquee",
"Backgrounds & Beams",
"Cards & Hover",
"Text Effects",
"Inputs & Buttons",
];
export default function UiGalleryPage() {
return (
<div className="flex w-full flex-col items-center bg-zinc-50 dark:bg-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">
50 components vendored straight from the{" "}
<a
href="https://ui.aceternity.com/components"
className="font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 hover:decoration-zinc-500 dark:text-zinc-200 dark:decoration-zinc-600"
target="_blank"
rel="noopener noreferrer"
>
Aceternity registry
</a>{" "}
and wired for this template. Everything below is live — hover, click and scroll.
</p>
<div className="flex flex-wrap gap-2 pt-1">
{SECTIONS.map((s) => (
<a
key={s}
href={`#${s.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`}
className="rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-300 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400 dark:hover:border-zinc-700 dark:hover:text-zinc-100"
>
{s}
</a>
))}
</div>
</div>
</div>
<main className="w-full max-w-6xl flex-1 px-6 py-12">
<Suspense
fallback={<div className="py-24 text-center text-sm text-zinc-400">Loading gallery…</div>}
>
<ShowcaseGallery />
</Suspense>
</main>
</div>
);
}
+2244
View File
File diff suppressed because it is too large. Load diff
+25
View File
@@ -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": {}
}
+18
View File
@@ -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>
);
}
+101
View File
@@ -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>
);
}
+98
View File
@@ -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>
);
}
+68
View File
@@ -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 · 50+ Aceternity components
</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>
);
}
+26
View File
@@ -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>
);
}
+733
View File
@@ -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&apos;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>
);
}
+121
View File
@@ -0,0 +1,121 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const ThreeDMarquee = ({ images, className }: { images: string[]; className?: string }) => {
// Split the images array into 4 equal parts
const chunkSize = Math.ceil(images.length / 4);
const chunks = Array.from({ length: 4 }, (_, colIndex) => {
const start = colIndex * chunkSize;
return images.slice(start, start + chunkSize);
});
return (
<div
className={cn("mx-auto block h-[600px] overflow-hidden rounded-2xl max-sm:h-100", className)}
>
<div className="flex size-full items-center justify-center">
<div className="size-[1720px] shrink-0 scale-50 sm:scale-75 lg:scale-100">
<div
style={{
transform: "rotateX(55deg) rotateY(0deg) rotateZ(-45deg)",
}}
className="relative top-96 right-[50%] grid size-full origin-top-left grid-cols-4 gap-8 transform-3d"
>
{chunks.map((subarray, colIndex) => (
<motion.div
animate={{ y: colIndex % 2 === 0 ? 100 : -100 }}
transition={{
duration: colIndex % 2 === 0 ? 10 : 15,
repeat: Infinity,
repeatType: "reverse",
}}
key={colIndex + "marquee"}
className="flex flex-col items-start gap-8"
>
<GridLineVertical className="-left-4" offset="80px" />
{subarray.map((image, imageIndex) => (
<div className="relative" key={imageIndex + image}>
<GridLineHorizontal className="-top-4" offset="20px" />
<motion.img
whileHover={{
y: -10,
}}
transition={{
duration: 0.3,
ease: "easeInOut",
}}
key={imageIndex + image}
src={image}
alt={`Image ${imageIndex + 1}`}
className="aspect-[970/700] rounded-lg object-cover ring ring-gray-950/5 hover:shadow-2xl"
width={970}
height={700}
/>
</div>
))}
</motion.div>
))}
</div>
</div>
</div>
</div>
);
};
const GridLineHorizontal = ({ className, offset }: { className?: string; offset?: string }) => {
return (
<div
style={
{
"--background": "#ffffff",
"--color": "rgba(0, 0, 0, 0.2)",
"--height": "1px",
"--width": "5px",
"--fade-stop": "90%",
"--offset": offset || "200px", //-100px if you want to keep the line inside
"--color-dark": "rgba(255, 255, 255, 0.2)",
maskComposite: "exclude",
} as React.CSSProperties
}
className={cn(
"absolute left-[calc(var(--offset)/2*-1)] h-[var(--height)] w-[calc(100%+var(--offset))]",
"bg-[linear-gradient(to_right,var(--color),var(--color)_50%,transparent_0,transparent)]",
"[background-size:var(--width)_var(--height)]",
"[mask:linear-gradient(to_left,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_right,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
"[mask-composite:exclude]",
"z-30",
"dark:bg-[linear-gradient(to_right,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
className
)}
></div>
);
};
const GridLineVertical = ({ className, offset }: { className?: string; offset?: string }) => {
return (
<div
style={
{
"--background": "#ffffff",
"--color": "rgba(0, 0, 0, 0.2)",
"--height": "5px",
"--width": "1px",
"--fade-stop": "90%",
"--offset": offset || "150px", //-100px if you want to keep the line inside
"--color-dark": "rgba(255, 255, 255, 0.2)",
maskComposite: "exclude",
} as React.CSSProperties
}
className={cn(
"absolute top-[calc(var(--offset)/2*-1)] h-[calc(100%+var(--offset))] w-[var(--width)]",
"bg-[linear-gradient(to_bottom,var(--color),var(--color)_50%,transparent_0,transparent)]",
"[background-size:var(--width)_var(--height)]",
"[mask:linear-gradient(to_top,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_bottom,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
"[mask-composite:exclude]",
"z-30",
"dark:bg-[linear-gradient(to_bottom,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
className
)}
></div>
);
};
+154
View File
@@ -0,0 +1,154 @@
"use client";
import React, { useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const PinContainer = ({
children,
title,
href,
className,
containerClassName,
}: {
children: React.ReactNode;
title?: string;
href?: string;
className?: string;
containerClassName?: string;
}) => {
const [transform, setTransform] = useState("translate(-50%,-50%) rotateX(0deg)");
const onMouseEnter = () => {
setTransform("translate(-50%,-50%) rotateX(40deg) scale(0.8)");
};
const onMouseLeave = () => {
setTransform("translate(-50%,-50%) rotateX(0deg) scale(1)");
};
return (
<a
className={cn("group/pin relative z-50 cursor-pointer", containerClassName)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
href={href || "/"}
>
<div
style={{
perspective: "1000px",
transform: "rotateX(70deg) translateZ(0deg)",
}}
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
>
<div
style={{
transform: transform,
}}
className="absolute top-1/2 left-1/2 flex items-start justify-start overflow-hidden rounded-2xl border border-white/[0.1] bg-black p-4 shadow-[0_8px_16px_rgb(0_0_0/0.4)] transition duration-700 group-hover/pin:border-white/[0.2]"
>
<div className={cn("relative z-50", className)}>{children}</div>
</div>
</div>
<PinPerspective title={title} href={href} />
</a>
);
};
export const PinPerspective = ({ title, href }: { title?: string; href?: string }) => {
return (
<motion.div className="pointer-events-none z-[60] flex h-80 w-96 items-center justify-center opacity-0 transition duration-500 group-hover/pin:opacity-100">
<div className="inset-0 -mt-7 h-full w-full flex-none">
<div className="absolute inset-x-0 top-0 flex justify-center">
<a
href={href}
target={"_blank"}
className="relative z-10 flex items-center space-x-2 rounded-full bg-zinc-950 px-4 py-0.5 ring-1 ring-white/10"
>
<span className="relative z-20 inline-block py-0.5 text-xs font-bold text-white">
{title}
</span>
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-emerald-400/90 to-emerald-400/0 transition-opacity duration-500 group-hover/btn:opacity-40"></span>
</a>
</div>
<div
style={{
perspective: "1000px",
transform: "rotateX(70deg) translateZ(0)",
}}
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
>
<>
<motion.div
initial={{
opacity: 0,
scale: 0,
x: "-50%",
y: "-50%",
}}
animate={{
opacity: [0, 1, 0.5, 0],
scale: 1,
z: 0,
}}
transition={{
duration: 6,
repeat: Infinity,
delay: 0,
}}
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
></motion.div>
<motion.div
initial={{
opacity: 0,
scale: 0,
x: "-50%",
y: "-50%",
}}
animate={{
opacity: [0, 1, 0.5, 0],
scale: 1,
z: 0,
}}
transition={{
duration: 6,
repeat: Infinity,
delay: 2,
}}
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
></motion.div>
<motion.div
initial={{
opacity: 0,
scale: 0,
x: "-50%",
y: "-50%",
}}
animate={{
opacity: [0, 1, 0.5, 0],
scale: 1,
z: 0,
}}
transition={{
duration: 6,
repeat: Infinity,
delay: 4,
}}
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
></motion.div>
</>
</div>
<>
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-cyan-500 blur-[2px] group-hover/pin:h-40" />
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-cyan-500 group-hover/pin:h-40" />
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[4px] w-[4px] translate-x-[1.5px] translate-y-[14px] rounded-full bg-cyan-600 blur-[3px]" />
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[2px] w-[2px] translate-x-[0.5px] translate-y-[14px] rounded-full bg-cyan-300" />
</>
</div>
</motion.div>
);
};
+69
View File
@@ -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 };
+162
View File
@@ -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,
};
+68
View File
@@ -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 };
+214
View File
@@ -0,0 +1,214 @@
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import React, { ReactNode, createContext, useContext, useEffect, useRef, useState } from "react";
interface ModalContextType {
open: boolean;
setOpen: (open: boolean) => void;
}
const ModalContext = createContext<ModalContextType | undefined>(undefined);
export const ModalProvider = ({ children }: { children: ReactNode }) => {
const [open, setOpen] = useState(false);
return <ModalContext.Provider value={{ open, setOpen }}>{children}</ModalContext.Provider>;
};
export const useModal = () => {
const context = useContext(ModalContext);
if (!context) {
throw new Error("useModal must be used within a ModalProvider");
}
return context;
};
export function Modal({ children }: { children: ReactNode }) {
return <ModalProvider>{children}</ModalProvider>;
}
export const ModalTrigger = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
const { setOpen } = useModal();
return (
<button
className={cn(
"relative overflow-hidden rounded-md px-4 py-2 text-center text-black dark:text-white",
className
)}
onClick={() => setOpen(true)}
>
{children}
</button>
);
};
export const ModalBody = ({ children, className }: { children: ReactNode; className?: string }) => {
const { open } = useModal();
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
}
}, [open]);
const modalRef = useRef<HTMLDivElement>(null);
const { setOpen } = useModal();
useOutsideClick(modalRef, () => setOpen(false));
return (
<AnimatePresence>
{open && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
backdropFilter: "blur(10px)",
}}
exit={{
opacity: 0,
backdropFilter: "blur(0px)",
}}
className="fixed inset-0 z-50 flex h-full w-full items-center justify-center [perspective:800px] [transform-style:preserve-3d]"
>
<Overlay />
<motion.div
ref={modalRef}
className={cn(
"relative z-50 flex max-h-[90%] min-h-[50%] flex-1 flex-col overflow-hidden border border-transparent bg-white md:max-w-[40%] md:rounded-2xl dark:border-neutral-800 dark:bg-neutral-950",
className
)}
initial={{
opacity: 0,
scale: 0.5,
rotateX: 40,
y: 40,
}}
animate={{
opacity: 1,
scale: 1,
rotateX: 0,
y: 0,
}}
exit={{
opacity: 0,
scale: 0.8,
rotateX: 10,
}}
transition={{
type: "spring",
stiffness: 260,
damping: 15,
}}
>
<CloseIcon />
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};
export const ModalContent = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
return <div className={cn("flex flex-1 flex-col p-8 md:p-10", className)}>{children}</div>;
};
export const ModalFooter = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
return (
<div className={cn("flex justify-end bg-gray-100 p-4 dark:bg-neutral-900", className)}>
{children}
</div>
);
};
const Overlay = ({ className }: { className?: string }) => {
return (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
backdropFilter: "blur(10px)",
}}
exit={{
opacity: 0,
backdropFilter: "blur(0px)",
}}
className={`bg-opacity-50 fixed inset-0 z-50 h-full w-full bg-black ${className}`}
></motion.div>
);
};
const CloseIcon = () => {
const { setOpen } = useModal();
return (
<button onClick={() => setOpen(false)} className="group absolute top-4 right-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-black transition duration-200 group-hover:scale-125 group-hover:rotate-3 dark:text-white"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M18 6l-12 12" />
<path d="M6 6l12 12" />
</svg>
</button>
);
};
// Hook to detect clicks outside of a component.
// Add it in a separate file, I've added here for simplicity
export const useOutsideClick = (
ref: React.RefObject<HTMLDivElement | null>,
callback: (event: MouseEvent | TouchEvent) => void
) => {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
// DO NOTHING if the element being clicked is the target element or their children
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
callback(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, callback]);
};
+122
View File
@@ -0,0 +1,122 @@
"use client";
import { useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
type Tab = {
title: string;
value: string;
content?: string | React.ReactNode | any;
};
export const Tabs = ({
tabs: propTabs,
containerClassName,
activeTabClassName,
tabClassName,
contentClassName,
}: {
tabs: Tab[];
containerClassName?: string;
activeTabClassName?: string;
tabClassName?: string;
contentClassName?: string;
}) => {
const [active, setActive] = useState<Tab>(propTabs[0]);
const [tabs, setTabs] = useState<Tab[]>(propTabs);
const moveSelectedTabToTop = (idx: number) => {
const newTabs = [...propTabs];
const selectedTab = newTabs.splice(idx, 1);
newTabs.unshift(selectedTab[0]);
setTabs(newTabs);
setActive(newTabs[0]);
};
const [hovering, setHovering] = useState(false);
return (
<>
<div
className={cn(
"no-visible-scrollbar relative flex w-full max-w-full flex-row items-center justify-start overflow-auto [perspective:1000px] sm:overflow-visible",
containerClassName
)}
>
{propTabs.map((tab, idx) => (
<button
key={tab.title}
onClick={() => {
moveSelectedTabToTop(idx);
}}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
className={cn("relative rounded-full px-4 py-2", tabClassName)}
style={{
transformStyle: "preserve-3d",
}}
>
{active.value === tab.value && (
<motion.div
layoutId="clickedbutton"
transition={{ type: "spring", bounce: 0.3, duration: 0.6 }}
className={cn(
"absolute inset-0 rounded-full bg-gray-200 dark:bg-zinc-800",
activeTabClassName
)}
/>
)}
<span className="relative block text-black dark:text-white">{tab.title}</span>
</button>
))}
</div>
<FadeInDiv
tabs={tabs}
active={active}
key={active.value}
hovering={hovering}
className={cn("mt-32", contentClassName)}
/>
</>
);
};
export const FadeInDiv = ({
className,
tabs,
hovering,
}: {
className?: string;
key?: string;
tabs: Tab[];
active: Tab;
hovering?: boolean;
}) => {
const isActive = (tab: Tab) => {
return tab.value === tabs[0].value;
};
return (
<div className="relative h-full w-full">
{tabs.map((tab, idx) => (
<motion.div
key={tab.value}
layoutId={tab.value}
style={{
scale: 1 - idx * 0.1,
top: hovering ? idx * -50 : 0,
zIndex: -idx,
opacity: idx < 3 ? 1 - idx * 0.1 : 0,
}}
animate={{
y: isActive(tab) ? [0, 40, 0] : 0,
}}
className={cn("absolute top-0 left-0 h-full w-full", className)}
>
{tab.content}
</motion.div>
))}
</div>
);
};
+85
View File
@@ -0,0 +1,85 @@
"use client";
import { useState, useRef } from "react";
import { motion, useTransform, AnimatePresence, useMotionValue, useSpring } from "motion/react";
export const AnimatedTooltip = ({
items,
}: {
items: {
id: number;
name: string;
designation: string;
image: string;
}[];
}) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const springConfig = { stiffness: 100, damping: 15 };
const x = useMotionValue(0);
const animationFrameRef = useRef<number | null>(null);
const rotate = useSpring(useTransform(x, [-100, 100], [-45, 45]), springConfig);
const translateX = useSpring(useTransform(x, [-100, 100], [-50, 50]), springConfig);
const handleMouseMove = (event: any) => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(() => {
const halfWidth = event.target.offsetWidth / 2;
x.set(event.nativeEvent.offsetX - halfWidth);
});
};
return (
<>
{items.map((item) => (
<div
className="group relative -mr-4"
key={item.name}
onMouseEnter={() => setHoveredIndex(item.id)}
onMouseLeave={() => setHoveredIndex(null)}
>
<AnimatePresence>
{hoveredIndex === item.id && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.6 }}
animate={{
opacity: 1,
y: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 260,
damping: 10,
},
}}
exit={{ opacity: 0, y: 20, scale: 0.6 }}
style={{
translateX: translateX,
rotate: rotate,
whiteSpace: "nowrap",
}}
className="absolute -top-16 left-1/2 z-50 flex -translate-x-1/2 flex-col items-center justify-center rounded-md bg-black px-4 py-2 text-xs shadow-xl"
>
<div className="absolute inset-x-10 -bottom-px z-30 h-px w-[20%] bg-gradient-to-r from-transparent via-emerald-500 to-transparent" />
<div className="absolute -bottom-px left-10 z-30 h-px w-[40%] bg-gradient-to-r from-transparent via-sky-500 to-transparent" />
<div className="relative z-30 text-base font-bold text-white">{item.name}</div>
<div className="text-xs text-white">{item.designation}</div>
</motion.div>
)}
</AnimatePresence>
<img
onMouseMove={handleMouseMove}
height={100}
width={100}
src={item.image}
alt={item.name}
className="relative !m-0 h-14 w-14 rounded-full border-2 border-white object-cover object-top !p-0 transition duration-500 group-hover:z-30 group-hover:scale-105"
/>
</div>
))}
</>
);
};
+22
View File
@@ -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 };
+195
View File
@@ -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,
};
+61
View File
@@ -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>
);
};
+92
View File
@@ -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 };
+139
View File
@@ -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";
+69
View File
@@ -0,0 +1,69 @@
"use client";
import React from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const BoxesCore = ({ className, ...rest }: { className?: string }) => {
const rows = new Array(150).fill(1);
const cols = new Array(100).fill(1);
const colors = [
"#93c5fd",
"#f9a8d4",
"#86efac",
"#fde047",
"#fca5a5",
"#d8b4fe",
"#93c5fd",
"#a5b4fc",
"#c4b5fd",
];
const getRandomColor = () => {
return colors[Math.floor(Math.random() * colors.length)];
};
return (
<div
style={{
transform: `translate(-40%,-60%) skewX(-48deg) skewY(14deg) scale(0.675) rotate(0deg) translateZ(0)`,
}}
className={cn(
"absolute -top-1/4 left-1/4 z-0 flex h-full w-full -translate-x-1/2 -translate-y-1/2 p-4",
className
)}
{...rest}
>
{rows.map((_, i) => (
<motion.div key={`row` + i} className="relative h-8 w-16 border-l border-slate-700">
{cols.map((_, j) => (
<motion.div
whileHover={{
backgroundColor: `${getRandomColor()}`,
transition: { duration: 0 },
}}
animate={{
transition: { duration: 2 },
}}
key={`col` + j}
className="relative h-8 w-16 border-t border-r border-slate-700"
>
{j % 2 === 0 && i % 2 === 0 ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="pointer-events-none absolute -top-[14px] -left-[22px] h-6 w-10 stroke-[1px] text-slate-700"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
</svg>
) : null}
</motion.div>
))}
</motion.div>
))}
</div>
);
};
export const Boxes = React.memo(BoxesCore);
@@ -0,0 +1,171 @@
"use client";
import { cn } from "@/lib/utils";
import { useEffect, useRef, useState } from "react";
export const BackgroundGradientAnimation = ({
gradientBackgroundStart = "rgb(108, 0, 162)",
gradientBackgroundEnd = "rgb(0, 17, 82)",
firstColor = "18, 113, 255",
secondColor = "221, 74, 255",
thirdColor = "100, 220, 255",
fourthColor = "200, 50, 50",
fifthColor = "180, 180, 50",
pointerColor = "140, 100, 255",
size = "80%",
blendingValue = "hard-light",
children,
className,
interactive = true,
containerClassName,
}: {
gradientBackgroundStart?: string;
gradientBackgroundEnd?: string;
firstColor?: string;
secondColor?: string;
thirdColor?: string;
fourthColor?: string;
fifthColor?: string;
pointerColor?: string;
size?: string;
blendingValue?: string;
children?: React.ReactNode;
className?: string;
interactive?: boolean;
containerClassName?: string;
}) => {
const interactiveRef = useRef<HTMLDivElement>(null);
const [curX, setCurX] = useState(0);
const [curY, setCurY] = useState(0);
const [tgX, setTgX] = useState(0);
const [tgY, setTgY] = useState(0);
useEffect(() => {
document.body.style.setProperty("--gradient-background-start", gradientBackgroundStart);
document.body.style.setProperty("--gradient-background-end", gradientBackgroundEnd);
document.body.style.setProperty("--first-color", firstColor);
document.body.style.setProperty("--second-color", secondColor);
document.body.style.setProperty("--third-color", thirdColor);
document.body.style.setProperty("--fourth-color", fourthColor);
document.body.style.setProperty("--fifth-color", fifthColor);
document.body.style.setProperty("--pointer-color", pointerColor);
document.body.style.setProperty("--size", size);
document.body.style.setProperty("--blending-value", blendingValue);
}, []);
useEffect(() => {
function move() {
if (!interactiveRef.current) {
return;
}
setCurX(curX + (tgX - curX) / 20);
setCurY(curY + (tgY - curY) / 20);
interactiveRef.current.style.transform = `translate(${Math.round(
curX
)}px, ${Math.round(curY)}px)`;
}
move();
}, [tgX, tgY]);
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (interactiveRef.current) {
const rect = interactiveRef.current.getBoundingClientRect();
setTgX(event.clientX - rect.left);
setTgY(event.clientY - rect.top);
}
};
const [isSafari, setIsSafari] = useState(false);
useEffect(() => {
setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent));
}, []);
return (
<div
className={cn(
"relative top-0 left-0 h-screen w-screen overflow-hidden bg-[linear-gradient(40deg,var(--gradient-background-start),var(--gradient-background-end))]",
containerClassName
)}
>
<svg className="hidden">
<defs>
<filter id="blurMe">
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
<feColorMatrix
in="blur"
mode="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -8"
result="goo"
/>
<feBlend in="SourceGraphic" in2="goo" />
</filter>
</defs>
</svg>
<div className={cn("", className)}>{children}</div>
<div
className={cn(
"gradients-container h-full w-full blur-lg",
isSafari ? "blur-2xl" : "[filter:url(#blurMe)_blur(40px)]"
)}
>
<div
className={cn(
`absolute [background:radial-gradient(circle_at_center,_var(--first-color)_0,_var(--first-color)_50%)_no-repeat]`,
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
`[transform-origin:center_center]`,
`animate-first`,
`opacity-100`
)}
></div>
<div
className={cn(
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--second-color),_0.8)_0,_rgba(var(--second-color),_0)_50%)_no-repeat]`,
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
`[transform-origin:calc(50%-400px)]`,
`animate-second`,
`opacity-100`
)}
></div>
<div
className={cn(
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--third-color),_0.8)_0,_rgba(var(--third-color),_0)_50%)_no-repeat]`,
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
`[transform-origin:calc(50%+400px)]`,
`animate-third`,
`opacity-100`
)}
></div>
<div
className={cn(
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--fourth-color),_0.8)_0,_rgba(var(--fourth-color),_0)_50%)_no-repeat]`,
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
`[transform-origin:calc(50%-200px)]`,
`animate-fourth`,
`opacity-70`
)}
></div>
<div
className={cn(
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--fifth-color),_0.8)_0,_rgba(var(--fifth-color),_0)_50%)_no-repeat]`,
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
`[transform-origin:calc(50%-800px)_calc(50%+800px)]`,
`animate-fifth`,
`opacity-100`
)}
></div>
{interactive && (
<div
ref={interactiveRef}
onMouseMove={handleMouseMove}
className={cn(
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--pointer-color),_0.8)_0,_rgba(var(--pointer-color),_0)_50%)_no-repeat]`,
`-top-1/2 -left-1/2 h-full w-full [mix-blend-mode:var(--blending-value)]`,
`opacity-70`
)}
></div>
)}
</div>
</div>
);
};
Loaded 50 of 173 files, more files were not shown because too many files have changed in this diff. Show more