90 Commits
Author SHA1 Message Date
asepharyana f1c7b09f09 fix: correct metadata, gallery counts, and README for Aceternity set (#23)
CI / lint + typecheck + test + build (push) Canceled after 0s
2026-09-22 17:10:11 +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 a70e08ac33 chore: sync bun.lock after dependabot bump 2026-09-20 03:15:24 +07:00
mytheclipsebotreview 31ba33ee4d chore: sync bun.lock after dependabot bump 2026-09-20 03:08:27 +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] 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
61 changed files with 7359 additions and 96 deletions
+37
View File
@@ -1,16 +1,53 @@
name: Dependabot Auto-Merge
on: pull_request
permissions:
contents: write
pull-requests: write
checks: read
jobs:
auto-merge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Verify CI check-runs before enabling auto-merge
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const sha = context.payload.pull_request.head.sha;
// Wait for in-progress runs (with timeout), then collect conclusions
const requiredContext = "lint + typecheck + test + build";
let conclusion = null;
for (let attempt = 0; attempt < 60; attempt++) {
const { data: checks } = await github.rest.checks.listForRef({
owner, repo, ref: sha,
});
const match = checks.check_runs.find(
(r) => r.name === requiredContext || r.output?.title === requiredContext
);
if (match && match.status === "completed") {
conclusion = match.conclusion;
break;
}
await new Promise((r) => setTimeout(r, 10000));
}
if (!conclusion) {
core.setFailed(`Required check "${requiredContext}" never completed within timeout.`);
return;
}
if (conclusion !== "success") {
core.setFailed(`Required check "${requiredContext}" concluded ${conclusion}. NOT merging.`);
return;
}
core.info(`Check "${requiredContext}" passed — safe to enable auto-merge.`);
- name: Enable auto-merge for Dependabot PR
if: success()
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
+39
View File
@@ -5,6 +5,8 @@ updates:
schedule:
interval: "daily"
open-pull-requests-limit: 10
labels:
- "dependencies"
groups:
production:
dependency-type: "production"
@@ -16,3 +18,40 @@ updates:
update-types:
- "minor"
- "patch"
ignore:
# ---- Toolchain pins: semver-major bumps break the lint/typecheck stack ----
# ESLint 10 dropped rule-context getFilename(); eslint-plugin-react (^7.37.x)
# peer range caps at eslint ^9.7 -> react/display-name crash at load.
- dependency-name: "eslint"
update-types: ["version-update:semver-major"]
# typescript-eslint 8.x peer range is ">=4.8.4 <6.1.0" -> TS 7 hard-refuses.
- dependency-name: "typescript"
update-types: ["version-update:semver-major"]
# @tsparticles v4 changed its exported API; the vendored SparklesCore from the
# Aceternity registry targets the v3 API (initParticlesEngine, IEffect.fill...).
# NOTE: dependabot `ignore` does NOT support wildcards (only `groups` does) —
# list each exact scoped package.
- dependency-name: "@tsparticles/react"
update-types: ["version-update:semver-major"]
- dependency-name: "@tsparticles/engine"
update-types: ["version-update:semver-major"]
- dependency-name: "@tsparticles/slim"
update-types: ["version-update:semver-major"]
# eslint-config-next majors follow Next.js majors and can pull incompatible
# plugin versions; framework majors are deliberate, not auto-bumps.
- dependency-name: "eslint-config-next"
update-types: ["version-update:semver-major"]
# eslint-plugin-react 8.x targets ESLint 10, which conflicts with the
# pinned eslint 9 (and the vendored components' validated toolchain).
- dependency-name: "eslint-plugin-react"
update-types: ["version-update:semver-major"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns: ["*"]
update-types:
- "minor"
- "patch"
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Set up Bun
uses: oven-sh/setup-bun@v2
+20 -1
View File
@@ -16,6 +16,24 @@ lint/typecheck/test/build pipeline.
- **Markdown:** react-markdown + remark-gfm + shiki (server-side highlighting)
- **Misc:** zustand, nuqs, sonner, cmdk, vaul, embla-carousel-react, date-fns,
next-themes, recharts
- **Motion effects:** 48 Aceternity UI effects vendored as source in
`components/ui/` (motion + @tabler/icons-react + @tsparticles + simplex-noise),
showcased live at `/ui`
## UI gallery (`/ui`)
48 Aceternity effects (3d-pin, bento-grid, sparkles, tracing-beam, …) vendored
verbatim from the [Aceternity registry](https://ui.aceternity.com/components)
and demoed live in `components/showcase/gallery.tsx`. The shadcn Base-Nova set
(accordions, dialogs, tables, …) ships alongside in the same `components/ui/`
dir — 110 files total.
| Rule | Detail |
| ------------- | ----------------------------------------------------------------------- |
| Source | Registry JSON, verbatim + `motion/react` / `cn` import fixes |
| Rename | Aceternity tabs → `animated-tabs.tsx` (collides with shadcn `tabs.tsx`) |
| Skipped | three.js trio, apple-cards-carousel (heavy deps, out of scope) |
| Overlay demos | `MultiStepLoader` demo is state-gated — never hardcode `loading` |
## Getting started
@@ -51,7 +69,8 @@ Git hooks (husky + lint-staged) run ESLint + Prettier on staged files.
```
app/
page.tsx # landing page
page.tsx # landing page (HomeHero: Cover + Sparkles + Spotlight)
ui/ # /ui gallery — 48 Aceternity effects, live demos
demo/ # wiring example: markdown+shiki, form, table, session
protected/ # route guarded by middleware (better-auth session)
error.tsx / loading.tsx / not-found.tsx
+6 -2
View File
@@ -14,8 +14,12 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: {
default: "Next.js Template — Aceternity + shadcn",
template: "%s — Next.js Template",
},
description:
"Production-ready Next.js 16 starter on Bun with 48 vendored Aceternity UI effects, shadcn Base-Nova components, Tailwind v4, motion, better-auth and a typed API client.",
};
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
+2 -73
View File
@@ -1,76 +1,5 @@
import Image from "next/image";
import Link from "next/link";
import { HomeHero } from "@/components/home-hero";
export default function Home() {
return (
<div className="flex flex-1 flex-col items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex w-full max-w-3xl flex-1 flex-col items-center justify-between bg-white px-16 py-32 sm:items-start dark:bg-black">
<Image
className="h-5 w-[100px] dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl leading-10 font-semibold tracking-tight text-black dark:text-zinc-50">
To get started, edit the{" "}
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
page.tsx
</code>{" "}
file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<Link
href="/demo"
className="rounded-full border border-black/[.08] bg-foreground px-5 py-2 text-sm font-medium text-background transition-colors hover:bg-[#383838] dark:border-white/[.145] dark:hover:bg-[#1a1a1a]"
>
View the demo wiring →
</Link>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] md:w-[158px] dark:hover:bg-[#ccc]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="h-[14px] w-4 dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={14}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] md:w-[158px] dark:border-white/[.145] dark:hover:bg-[#1a1a1a]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
return <HomeHero />;
}
+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 + shadcn",
description:
"Forty-eight Aceternity UI effects vendored into the template — animated cards, beams, backgrounds, pins, marquees and more, wired for the App Router, alongside the full shadcn Base-Nova set.",
};
const SECTIONS = [
"3D Pin / Marquee",
"Backgrounds & Beams",
"Cards & Hover",
"Text Effects",
"Inputs & Buttons",
];
export default function UiGalleryPage() {
return (
<div className="flex w-full flex-col items-center bg-zinc-50 dark:bg-zinc-950 dark:bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.04),transparent_55%)]">
<div className="w-full border-b border-zinc-200 bg-white/80 py-10 backdrop-blur-sm dark:border-zinc-800 dark:bg-zinc-950/80">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-3 px-6">
<nav className="text-xs font-medium tracking-wide text-zinc-500 dark:text-zinc-400">
<Link
href="/"
className="transition-colors hover:text-zinc-900 dark:hover:text-zinc-100"
>
Home
</Link>
<span className="px-2">/</span>
<span className="text-zinc-800 dark:text-zinc-200">UI Gallery</span>
</nav>
<h1 className="text-3xl font-semibold tracking-tight text-balance text-zinc-900 sm:text-4xl dark:text-zinc-50">
Aceternity UI gallery
</h1>
<p className="max-w-2xl text-sm leading-6 text-zinc-600 dark:text-zinc-400">
48 Aceternity effects vendored straight from the{" "}
<a
href="https://ui.aceternity.com/components"
className="font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 hover:decoration-zinc-500 dark:text-zinc-200 dark:decoration-zinc-600"
target="_blank"
rel="noopener noreferrer"
>
Aceternity registry
</a>{" "}
and wired for this template. Everything below is live — hover, click and scroll.
</p>
<div className="flex flex-wrap gap-2 pt-1">
{SECTIONS.map((s) => (
<a
key={s}
href={`#${s.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`}
className="rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-300 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400 dark:hover:border-zinc-700 dark:hover:text-zinc-100"
>
{s}
</a>
))}
</div>
</div>
</div>
<main className="w-full max-w-6xl flex-1 px-6 py-12">
<Suspense
fallback={<div className="py-24 text-center text-sm text-zinc-400">Loading gallery…</div>}
>
<ShowcaseGallery />
</Suspense>
</main>
</div>
);
}
+102 -16
View File
File diff suppressed because one or more lines are too long
+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 · 48 Aceternity effects + shadcn
</span>
<h1 className="max-w-3xl text-4xl leading-tight font-semibold tracking-tight text-zinc-900 sm:text-6xl dark:text-white">
A template that ships with <Cover>animated UI out of the box.</Cover>
</h1>
<p className="max-w-xl text-base leading-7 text-zinc-600 dark:text-zinc-300">
Aceternity components vendored as source, Tailwind v4 native, zero-warning lint. Every
effect below is live — build a landing page without starting from scratch.
</p>
<div className="flex flex-wrap items-center justify-center gap-4">
<Link href="/ui" passHref>
<MagneticButton strength={0.5} maxDistance={60}>
<span className="inline-flex items-center gap-2 rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200">
Browse the gallery
<span aria-hidden>→</span>
</span>
</MagneticButton>
</Link>
<Link
href="/demo"
className="inline-flex items-center gap-2 rounded-full border border-zinc-300 px-6 py-3 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-900"
>
View the demo wiring
</Link>
</div>
</motion.main>
</div>
);
}
+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>
);
};
+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>
))}
</>
);
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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>
);
};
+72
View File
@@ -0,0 +1,72 @@
import { cn } from "@/lib/utils";
import React from "react";
import { motion } from "motion/react";
export const BackgroundGradient = ({
children,
className,
containerClassName,
animate = true,
}: {
children?: React.ReactNode;
className?: string;
containerClassName?: string;
animate?: boolean;
}) => {
const variants = {
initial: {
backgroundPosition: "0 50%",
},
animate: {
backgroundPosition: ["0, 50%", "100% 50%", "0 50%"],
},
};
return (
<div className={cn("group relative p-[4px]", containerClassName)}>
<motion.div
variants={animate ? variants : undefined}
initial={animate ? "initial" : undefined}
animate={animate ? "animate" : undefined}
transition={
animate
? {
duration: 5,
repeat: Infinity,
repeatType: "reverse",
}
: undefined
}
style={{
backgroundSize: animate ? "400% 400%" : undefined,
}}
className={cn(
"absolute inset-0 z-[1] rounded-3xl opacity-60 blur-xl transition duration-500 will-change-transform group-hover:opacity-100",
"bg-[radial-gradient(circle_farthest-side_at_0_100%,#00ccb1,transparent),radial-gradient(circle_farthest-side_at_100%_0,#7b61ff,transparent),radial-gradient(circle_farthest-side_at_100%_100%,#ffc414,transparent),radial-gradient(circle_farthest-side_at_0_0,#1ca0fb,#141316)]"
)}
/>
<motion.div
variants={animate ? variants : undefined}
initial={animate ? "initial" : undefined}
animate={animate ? "animate" : undefined}
transition={
animate
? {
duration: 5,
repeat: Infinity,
repeatType: "reverse",
}
: undefined
}
style={{
backgroundSize: animate ? "400% 400%" : undefined,
}}
className={cn(
"absolute inset-0 z-[1] rounded-3xl will-change-transform",
"bg-[radial-gradient(circle_farthest-side_at_0_100%,#00ccb1,transparent),radial-gradient(circle_farthest-side_at_100%_0,#7b61ff,transparent),radial-gradient(circle_farthest-side_at_100%_100%,#ffc414,transparent),radial-gradient(circle_farthest-side_at_0_0,#1ca0fb,#141316)]"
)}
/>
<div className={cn("relative z-10", className)}>{children}</div>
</div>
);
};
+54
View File
@@ -0,0 +1,54 @@
import { cn } from "@/lib/utils";
export const BentoGrid = ({
className,
children,
}: {
className?: string;
children?: React.ReactNode;
}) => {
return (
<div
className={cn(
"mx-auto grid max-w-7xl grid-cols-1 gap-4 md:auto-rows-[18rem] md:grid-cols-3",
className
)}
>
{children}
</div>
);
};
export const BentoGridItem = ({
className,
title,
description,
header,
icon,
}: {
className?: string;
title?: string | React.ReactNode;
description?: string | React.ReactNode;
header?: React.ReactNode;
icon?: React.ReactNode;
}) => {
return (
<div
className={cn(
"group/bento row-span-1 flex flex-col justify-between space-y-4 rounded-xl border border-neutral-200 bg-white p-4 shadow-input transition duration-200 hover:shadow-xl dark:border-white/[0.2] dark:bg-black dark:shadow-none",
className
)}
>
{header}
<div className="transition duration-200 group-hover/bento:translate-x-2">
{icon}
<div className="mt-2 mb-2 font-sans font-bold text-neutral-600 dark:text-neutral-200">
{title}
</div>
<div className="font-sans text-xs font-normal text-neutral-600 dark:text-neutral-300">
{description}
</div>
</div>
</div>
);
};
+99
View File
@@ -0,0 +1,99 @@
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
export const HoverEffect = ({
items,
className,
}: {
items: {
title: string;
description: string;
link: string;
}[];
className?: string;
}) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
return (
<div className={cn("grid grid-cols-1 py-10 md:grid-cols-2 lg:grid-cols-3", className)}>
{items.map((item, idx) => (
<a
href={item?.link}
key={item?.link}
className="group relative block h-full w-full p-2"
onMouseEnter={() => setHoveredIndex(idx)}
onMouseLeave={() => setHoveredIndex(null)}
>
<AnimatePresence>
{hoveredIndex === idx && (
<motion.span
className="absolute inset-0 block h-full w-full rounded-3xl bg-neutral-200 dark:bg-slate-800/[0.8]"
layoutId="hoverBackground"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { duration: 0.15 },
}}
exit={{
opacity: 0,
transition: { duration: 0.15, delay: 0.2 },
}}
/>
)}
</AnimatePresence>
<Card>
<CardTitle>{item.title}</CardTitle>
<CardDescription>{item.description}</CardDescription>
</Card>
</a>
))}
</div>
);
};
export const Card = ({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) => {
return (
<div
className={cn(
"relative z-20 h-full w-full overflow-hidden rounded-2xl border border-transparent bg-black p-4 group-hover:border-slate-700 dark:border-white/[0.2]",
className
)}
>
<div className="relative z-50">
<div className="p-4">{children}</div>
</div>
</div>
);
};
export const CardTitle = ({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) => {
return (
<h4 className={cn("mt-4 font-bold tracking-wide text-zinc-100", className)}>{children}</h4>
);
};
export const CardDescription = ({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) => {
return (
<p className={cn("mt-8 text-sm leading-relaxed tracking-wide text-zinc-400", className)}>
{children}
</p>
);
};
+61
View File
@@ -0,0 +1,61 @@
"use client";
import { useMotionValue, motion, useMotionTemplate } from "motion/react";
import React, { MouseEvent as ReactMouseEvent, useState } from "react";
import { cn } from "@/lib/utils";
export const CardSpotlight = ({
children,
radius = 350,
color = "#262626",
className,
...props
}: {
radius?: number;
color?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>) => {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
function handleMouseMove({ currentTarget, clientX, clientY }: ReactMouseEvent<HTMLDivElement>) {
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top);
}
const [isHovering, setIsHovering] = useState(false);
const handleMouseEnter = () => setIsHovering(true);
const handleMouseLeave = () => setIsHovering(false);
return (
<div
className={cn(
"group/spotlight relative rounded-md border border-neutral-800 bg-black p-10 dark:border-neutral-800",
className
)}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
>
<motion.div
className="pointer-events-none absolute -inset-px z-0 rounded-md opacity-0 transition duration-300 group-hover/spotlight:opacity-100"
style={{
backgroundColor: color,
maskImage: useMotionTemplate`
radial-gradient(
${radius}px circle at ${mouseX}px ${mouseY}px,
white,
transparent 80%
)
`,
}}
>
{isHovering && (
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_var(--spotlight-x,50%)_var(--spotlight-y,50%),rgba(59,130,246,0.35),transparent_60%)]" />
)}
</motion.div>
{children}
</div>
);
};
+65
View File
@@ -0,0 +1,65 @@
"use client";
import { useEffect, useState } from "react";
import { motion } from "motion/react";
type Card = {
id: number;
name: string;
designation: string;
content: React.ReactNode;
};
export const CardStack = ({
items,
offset,
scaleFactor,
}: {
items: Card[];
offset?: number;
scaleFactor?: number;
}) => {
const CARD_OFFSET = offset || 10;
const SCALE_FACTOR = scaleFactor || 0.06;
const [cards, setCards] = useState<Card[]>(items);
useEffect(() => {
const interval = setInterval(() => {
setCards((prevCards: Card[]) => {
const newArray = [...prevCards]; // create a copy of the array
newArray.unshift(newArray.pop()!); // move the last element to the front
return newArray;
});
}, 5000);
return () => clearInterval(interval);
}, []);
return (
<div className="relative h-60 w-60 md:h-60 md:w-96">
{cards.map((card, index) => {
return (
<motion.div
key={card.id}
className="absolute flex h-60 w-60 flex-col justify-between rounded-3xl border border-neutral-200 bg-white p-4 shadow-xl shadow-black/[0.1] md:h-60 md:w-96 dark:border-white/[0.1] dark:bg-black dark:shadow-white/[0.05]"
style={{
transformOrigin: "top center",
}}
animate={{
top: index * -CARD_OFFSET,
scale: 1 - index * SCALE_FACTOR, // decrease scale for cards that are behind
zIndex: cards.length - index, // decrease z-index for the cards that are behind
}}
>
<div className="font-normal text-neutral-700 dark:text-neutral-200">{card.content}</div>
<div>
<p className="font-medium text-neutral-500 dark:text-white">{card.name}</p>
<p className="font-normal text-neutral-400 dark:text-neutral-200">
{card.designation}
</p>
</div>
</motion.div>
);
})}
</div>
);
};
@@ -0,0 +1,95 @@
"use client";
import React, { useRef } from "react";
import { useScroll, useTransform, motion, MotionValue } from "motion/react";
export const ContainerScroll = ({
titleComponent,
children,
}: {
titleComponent: string | React.ReactNode;
children: React.ReactNode;
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
});
const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth <= 768);
};
checkMobile();
window.addEventListener("resize", checkMobile);
return () => {
window.removeEventListener("resize", checkMobile);
};
}, []);
const scaleDimensions = () => {
return isMobile ? [0.7, 0.9] : [1.05, 1];
};
const rotate = useTransform(scrollYProgress, [0, 1], [20, 0]);
const scale = useTransform(scrollYProgress, [0, 1], scaleDimensions());
const translate = useTransform(scrollYProgress, [0, 1], [0, -100]);
return (
<div
className="relative flex h-[60rem] items-center justify-center p-2 md:h-[80rem] md:p-20"
ref={containerRef}
>
<div
className="relative w-full py-10 md:py-40"
style={{
perspective: "1000px",
}}
>
<Header translate={translate} titleComponent={titleComponent} />
<Card rotate={rotate} translate={translate} scale={scale}>
{children}
</Card>
</div>
</div>
);
};
export const Header = ({ translate, titleComponent }: any) => {
return (
<motion.div
style={{
translateY: translate,
}}
className="div mx-auto max-w-5xl text-center"
>
{titleComponent}
</motion.div>
);
};
export const Card = ({
rotate,
scale,
children,
}: {
rotate: MotionValue<number>;
scale: MotionValue<number>;
translate: MotionValue<number>;
children: React.ReactNode;
}) => {
return (
<motion.div
style={{
rotateX: rotate,
scale,
boxShadow:
"0 0 #0000004d, 0 9px 20px #0000004a, 0 37px 37px #00000042, 0 84px 50px #00000026, 0 149px 60px #0000000a, 0 233px 65px #00000003",
}}
className="mx-auto -mt-12 h-[30rem] w-full max-w-5xl rounded-[30px] border-4 border-[#6C6C6C] bg-[#222222] p-2 shadow-2xl md:h-[40rem] md:p-6"
>
<div className="h-full w-full overflow-hidden rounded-2xl bg-gray-100 md:rounded-2xl md:p-4 dark:bg-zinc-900">
{children}
</div>
</motion.div>
);
};
+219
View File
@@ -0,0 +1,219 @@
"use client";
import React, { useEffect, useId, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { useRef } from "react";
import { cn } from "@/lib/utils";
import { SparklesCore } from "@/components/ui/sparkles";
export const Cover = ({
children,
className,
}: {
children?: React.ReactNode;
className?: string;
}) => {
const [hovered, setHovered] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const [containerWidth, setContainerWidth] = useState(0);
const [beamPositions, setBeamPositions] = useState<number[]>([]);
useEffect(() => {
if (ref.current) {
setContainerWidth(ref.current?.clientWidth ?? 0);
const height = ref.current?.clientHeight ?? 0;
const numberOfBeams = Math.floor(height / 10); // Adjust the divisor to control the spacing
const positions = Array.from(
{ length: numberOfBeams },
(_, i) => (i + 1) * (height / (numberOfBeams + 1))
);
setBeamPositions(positions);
}
}, [ref.current]);
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
ref={ref}
className="group/cover relative inline-block rounded-sm bg-neutral-100 px-2 py-2 transition duration-200 hover:bg-neutral-900 dark:bg-neutral-900"
>
<AnimatePresence>
{hovered && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
opacity: {
duration: 0.2,
},
}}
className="absolute inset-0 h-full w-full overflow-hidden"
>
<motion.div
animate={{
translateX: ["-50%", "0%"],
}}
transition={{
translateX: {
duration: 10,
ease: "linear",
repeat: Infinity,
},
}}
className="flex h-full w-[200%]"
>
<SparklesCore
background="transparent"
minSize={0.4}
maxSize={1}
particleDensity={500}
className="h-full w-full"
particleColor="#FFFFFF"
/>
<SparklesCore
background="transparent"
minSize={0.4}
maxSize={1}
particleDensity={500}
className="h-full w-full"
particleColor="#FFFFFF"
/>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{beamPositions.map((position, index) => (
<Beam
key={index}
hovered={hovered}
duration={Math.random() * 2 + 1}
delay={Math.random() * 2 + 1}
width={containerWidth}
style={{
top: `${position}px`,
}}
/>
))}
<motion.span
key={String(hovered)}
animate={{
scale: hovered ? 0.8 : 1,
x: hovered ? [0, -30, 30, -30, 30, 0] : 0,
y: hovered ? [0, 30, -30, 30, -30, 0] : 0,
}}
exit={{
filter: "none",
scale: 1,
x: 0,
y: 0,
}}
transition={{
duration: 0.2,
x: {
duration: 0.2,
repeat: Infinity,
repeatType: "loop",
},
y: {
duration: 0.2,
repeat: Infinity,
repeatType: "loop",
},
scale: {
duration: 0.2,
},
filter: {
duration: 0.2,
},
}}
className={cn(
"relative z-20 inline-block text-neutral-900 transition duration-200 group-hover/cover:text-white dark:text-white",
className
)}
>
{children}
</motion.span>
<CircleIcon className="absolute -top-[2px] -right-[2px]" />
<CircleIcon className="absolute -right-[2px] -bottom-[2px]" />
<CircleIcon className="absolute -top-[2px] -left-[2px]" />
<CircleIcon className="absolute -bottom-[2px] -left-[2px]" />
</div>
);
};
export const Beam = ({
className,
delay,
duration,
hovered,
width = 600,
...svgProps
}: {
className?: string;
delay?: number;
duration?: number;
hovered?: boolean;
width?: number;
} & React.ComponentProps<typeof motion.svg>) => {
const id = useId();
return (
<motion.svg
width={width ?? "600"}
height="1"
viewBox={`0 0 ${width ?? "600"} 1`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn("absolute inset-x-0 w-full", className)}
{...svgProps}
>
<motion.path d={`M0 0.5H${width ?? "600"}`} stroke={`url(#svgGradient-${id})`} />
<defs>
<motion.linearGradient
id={`svgGradient-${id}`}
key={String(hovered)}
gradientUnits="userSpaceOnUse"
initial={{
x1: "0%",
x2: hovered ? "-10%" : "-5%",
y1: 0,
y2: 0,
}}
animate={{
x1: "110%",
x2: hovered ? "100%" : "105%",
y1: 0,
y2: 0,
}}
transition={{
duration: hovered ? 0.5 : (duration ?? 2),
ease: "linear",
repeat: Infinity,
delay: hovered ? Math.random() * (1 - 0.2) + 0.2 : 0,
repeatDelay: hovered ? Math.random() * (2 - 1) + 1 : (delay ?? 1),
}}
>
<stop stopColor="#2EB9DF" stopOpacity="0" />
<stop stopColor="#3b82f6" />
<stop offset="1" stopColor="#3b82f6" stopOpacity="0" />
</motion.linearGradient>
</defs>
</motion.svg>
);
};
export const CircleIcon = ({ className }: { className?: string }) => {
return (
<div
className={cn(
`group pointer-events-none h-2 w-2 animate-pulse rounded-full bg-neutral-600 opacity-20 group-hover/cover:hidden group-hover/cover:bg-white group-hover/cover:opacity-100 dark:bg-white`,
className
)}
></div>
);
};
+156
View File
@@ -0,0 +1,156 @@
"use client";
import { useRef, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/lib/utils";
export const DirectionAwareHover = ({
imageUrl,
children,
childrenClassName,
imageClassName,
className,
}: {
imageUrl: string;
children: React.ReactNode | string;
childrenClassName?: string;
imageClassName?: string;
className?: string;
}) => {
const ref = useRef<HTMLDivElement>(null);
const [direction, setDirection] = useState<"top" | "bottom" | "left" | "right" | string>("left");
const handleMouseEnter = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (!ref.current) return;
const direction = getDirection(event, ref.current);
console.log("direction", direction);
switch (direction) {
case 0:
setDirection("top");
break;
case 1:
setDirection("right");
break;
case 2:
setDirection("bottom");
break;
case 3:
setDirection("left");
break;
default:
setDirection("left");
break;
}
};
const getDirection = (ev: React.MouseEvent<HTMLDivElement, MouseEvent>, obj: HTMLElement) => {
const { width: w, height: h, left, top } = obj.getBoundingClientRect();
const x = ev.clientX - left - (w / 2) * (w > h ? h / w : 1);
const y = ev.clientY - top - (h / 2) * (h > w ? w / h : 1);
const d = Math.round(Math.atan2(y, x) / 1.57079633 + 5) % 4;
return d;
};
return (
<motion.div
onMouseEnter={handleMouseEnter}
ref={ref}
className={cn(
"group/card relative h-60 w-60 overflow-hidden rounded-lg bg-transparent md:h-96 md:w-96",
className
)}
>
<AnimatePresence mode="wait">
<motion.div
className="relative h-full w-full"
initial="initial"
whileHover={direction}
exit="exit"
>
<motion.div className="absolute inset-0 z-10 hidden h-full w-full bg-black/40 transition duration-500 group-hover/card:block" />
<motion.div
variants={variants}
className="relative h-full w-full bg-gray-50 dark:bg-black"
transition={{
duration: 0.2,
ease: "easeOut",
}}
>
<img
alt="image"
className={cn("h-full w-full scale-[1.15] object-cover", imageClassName)}
width="1000"
height="1000"
src={imageUrl}
/>
</motion.div>
<motion.div
variants={textVariants}
transition={{
duration: 0.5,
ease: "easeOut",
}}
className={cn("absolute bottom-4 left-4 z-40 text-white", childrenClassName)}
>
{children}
</motion.div>
</motion.div>
</AnimatePresence>
</motion.div>
);
};
const variants = {
initial: {
x: 0,
},
exit: {
x: 0,
y: 0,
},
top: {
y: 20,
},
bottom: {
y: -20,
},
left: {
x: 20,
},
right: {
x: -20,
},
};
const textVariants = {
initial: {
y: 0,
x: 0,
opacity: 0,
},
exit: {
y: 0,
x: 0,
opacity: 0,
},
top: {
y: -20,
opacity: 1,
},
bottom: {
y: 2,
opacity: 1,
},
left: {
x: -2,
opacity: 1,
},
right: {
x: 20,
opacity: 1,
},
};
+141
View File
@@ -0,0 +1,141 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { motion, useInView } from "motion/react";
import { cn } from "@/lib/utils";
type EncryptedTextProps = {
text: string;
className?: string;
/**
* Time in milliseconds between revealing each subsequent real character.
* Lower is faster. Defaults to 50ms per character.
*/
revealDelayMs?: number;
/** Optional custom character set to use for the gibberish effect. */
charset?: string;
/**
* Time in milliseconds between gibberish flips for unrevealed characters.
* Lower is more jittery. Defaults to 50ms.
*/
flipDelayMs?: number;
/** CSS class for styling the encrypted/scrambled characters */
encryptedClassName?: string;
/** CSS class for styling the revealed characters */
revealedClassName?: string;
};
const DEFAULT_CHARSET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-={}[];:,.<>/?";
function generateRandomCharacter(charset: string): string {
const index = Math.floor(Math.random() * charset.length);
return charset.charAt(index);
}
function generateGibberishPreservingSpaces(original: string, charset: string): string {
if (!original) return "";
let result = "";
for (let i = 0; i < original.length; i += 1) {
const ch = original[i];
result += ch === " " ? " " : generateRandomCharacter(charset);
}
return result;
}
export const EncryptedText: React.FC<EncryptedTextProps> = ({
text,
className,
revealDelayMs = 50,
charset = DEFAULT_CHARSET,
flipDelayMs = 50,
encryptedClassName,
revealedClassName,
}) => {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
const [revealCount, setRevealCount] = useState<number>(0);
const animationFrameRef = useRef<number | null>(null);
const startTimeRef = useRef<number>(0);
const lastFlipTimeRef = useRef<number>(0);
const scrambleCharsRef = useRef<string[]>(
text ? generateGibberishPreservingSpaces(text, charset).split("") : []
);
useEffect(() => {
if (!isInView) return;
// Reset state for a fresh animation whenever dependencies change
const initial = text ? generateGibberishPreservingSpaces(text, charset) : "";
scrambleCharsRef.current = initial.split("");
startTimeRef.current = performance.now();
lastFlipTimeRef.current = startTimeRef.current;
setRevealCount(0);
let isCancelled = false;
const update = (now: number) => {
if (isCancelled) return;
const elapsedMs = now - startTimeRef.current;
const totalLength = text.length;
const currentRevealCount = Math.min(
totalLength,
Math.floor(elapsedMs / Math.max(1, revealDelayMs))
);
setRevealCount(currentRevealCount);
if (currentRevealCount >= totalLength) {
return;
}
// Re-randomize unrevealed scramble characters on an interval
const timeSinceLastFlip = now - lastFlipTimeRef.current;
if (timeSinceLastFlip >= Math.max(0, flipDelayMs)) {
for (let index = 0; index < totalLength; index += 1) {
if (index >= currentRevealCount) {
if (text[index] !== " ") {
scrambleCharsRef.current[index] = generateRandomCharacter(charset);
} else {
scrambleCharsRef.current[index] = " ";
}
}
}
lastFlipTimeRef.current = now;
}
animationFrameRef.current = requestAnimationFrame(update);
};
animationFrameRef.current = requestAnimationFrame(update);
return () => {
isCancelled = true;
if (animationFrameRef.current !== null) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [isInView, text, revealDelayMs, charset, flipDelayMs]);
if (!text) return null;
return (
<motion.span ref={ref} className={cn(className)} aria-label={text} role="text">
{text.split("").map((char, index) => {
const isRevealed = index < revealCount;
const displayChar = isRevealed
? char
: char === " "
? " "
: (scrambleCharsRef.current[index] ?? generateRandomCharacter(charset));
return (
<span key={index} className={cn(isRevealed ? revealedClassName : encryptedClassName)}>
{displayChar}
</span>
);
})}
</motion.span>
);
};
+96
View File
@@ -0,0 +1,96 @@
"use client";
import { useMotionValue } from "motion/react";
import { useState, useEffect } from "react";
import { useMotionTemplate, motion } from "motion/react";
import { cn } from "@/lib/utils";
export const EvervaultCard = ({ text, className }: { text?: string; className?: string }) => {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const [randomString, setRandomString] = useState("");
useEffect(() => {
const str = generateRandomString(1500);
setRandomString(str);
}, []);
function onMouseMove({ currentTarget, clientX, clientY }: any) {
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top);
const str = generateRandomString(1500);
setRandomString(str);
}
return (
<div
className={cn(
"relative flex aspect-square h-full w-full items-center justify-center bg-transparent p-0.5",
className
)}
>
<div
onMouseMove={onMouseMove}
className="group/card relative flex h-full w-full items-center justify-center overflow-hidden rounded-3xl bg-transparent"
>
<CardPattern mouseX={mouseX} mouseY={mouseY} randomString={randomString} />
<div className="relative z-10 flex items-center justify-center">
<div className="relative flex h-44 w-44 items-center justify-center rounded-full text-4xl font-bold text-white">
<div className="absolute h-full w-full rounded-full bg-white/[0.8] blur-sm dark:bg-black/[0.8]" />
<span className="z-20 text-black dark:text-white">{text}</span>
</div>
</div>
</div>
</div>
);
};
export function CardPattern({ mouseX, mouseY, randomString }: any) {
const maskImage = useMotionTemplate`radial-gradient(250px at ${mouseX}px ${mouseY}px, white, transparent)`;
const style = { maskImage, WebkitMaskImage: maskImage };
return (
<div className="pointer-events-none">
<div className="absolute inset-0 rounded-2xl [mask-image:linear-gradient(white,transparent)] group-hover/card:opacity-50"></div>
<motion.div
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-green-500 to-blue-700 opacity-0 backdrop-blur-xl transition duration-500 group-hover/card:opacity-100"
style={style}
/>
<motion.div
className="absolute inset-0 rounded-2xl opacity-0 mix-blend-overlay group-hover/card:opacity-100"
style={style}
>
<p className="absolute inset-x-0 h-full font-mono text-xs font-bold break-words whitespace-pre-wrap text-white transition duration-500">
{randomString}
</p>
</motion.div>
</div>
);
}
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
export const generateRandomString = (length: number) => {
let result = "";
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
};
export const Icon = ({ className, ...rest }: any) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className={className}
{...rest}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
</svg>
);
};
+98
View File
@@ -0,0 +1,98 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/lib/utils";
export const FlipWords = ({
words,
duration = 3000,
className,
}: {
words: string[];
duration?: number;
className?: string;
}) => {
const [currentWord, setCurrentWord] = useState(words[0]);
const [isAnimating, setIsAnimating] = useState<boolean>(false);
// thanks for the fix Julian - https://github.com/Julian-AT
const startAnimation = useCallback(() => {
const word = words[words.indexOf(currentWord) + 1] || words[0];
setCurrentWord(word);
setIsAnimating(true);
}, [currentWord, words]);
useEffect(() => {
if (!isAnimating)
setTimeout(() => {
startAnimation();
}, duration);
}, [isAnimating, duration, startAnimation]);
return (
<AnimatePresence
onExitComplete={() => {
setIsAnimating(false);
}}
>
<motion.div
initial={{
opacity: 0,
y: 10,
}}
animate={{
opacity: 1,
y: 0,
}}
transition={{
type: "spring",
stiffness: 100,
damping: 10,
}}
exit={{
opacity: 0,
y: -40,
x: 40,
filter: "blur(8px)",
scale: 2,
position: "absolute",
}}
className={cn(
"relative z-10 inline-block px-2 text-left text-neutral-900 dark:text-neutral-100",
className
)}
key={currentWord}
>
{/* edit suggested by Sajal: https://x.com/DewanganSajal */}
{currentWord.split(" ").map((word, wordIndex) => (
<motion.span
key={word + wordIndex}
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{
delay: wordIndex * 0.3,
duration: 0.3,
}}
className="inline-block whitespace-nowrap"
>
{word.split("").map((letter, letterIndex) => (
<motion.span
key={word + letterIndex}
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{
delay: wordIndex * 0.3 + letterIndex * 0.05,
duration: 0.2,
}}
className="inline-block"
>
{letter}
</motion.span>
))}
<span className="inline-block">&nbsp;</span>
</motion.span>
))}
</motion.div>
</AnimatePresence>
);
};
+196
View File
@@ -0,0 +1,196 @@
"use client";
/**
* Note: Use position fixed according to your needs
* Desktop navbar is better positioned at the bottom
* Mobile navbar is better positioned at bottom right.
**/
import { cn } from "@/lib/utils";
import { IconLayoutNavbarCollapse } from "@tabler/icons-react";
import {
AnimatePresence,
MotionValue,
motion,
useMotionValue,
useSpring,
useTransform,
} from "motion/react";
import { useRef, useState } from "react";
export const FloatingDock = ({
items,
desktopClassName,
mobileClassName,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
desktopClassName?: string;
mobileClassName?: string;
}) => {
return (
<>
<FloatingDockDesktop items={items} className={desktopClassName} />
<FloatingDockMobile items={items} className={mobileClassName} />
</>
);
};
const FloatingDockMobile = ({
items,
className,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
className?: string;
}) => {
const [open, setOpen] = useState(false);
return (
<div className={cn("relative block md:hidden", className)}>
<AnimatePresence>
{open && (
<motion.div
layoutId="nav"
className="absolute inset-x-0 bottom-full mb-2 flex flex-col gap-2"
>
{items.map((item, idx) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 10 }}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: 10,
transition: {
delay: idx * 0.05,
},
}}
transition={{ delay: (items.length - 1 - idx) * 0.05 }}
>
<a
href={item.href}
key={item.title}
className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 dark:bg-neutral-900"
>
<div className="h-4 w-4">{item.icon}</div>
</a>
</motion.div>
))}
</motion.div>
)}
</AnimatePresence>
<button
onClick={() => setOpen(!open)}
className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 dark:bg-neutral-800"
>
<IconLayoutNavbarCollapse className="h-5 w-5 text-neutral-500 dark:text-neutral-400" />
</button>
</div>
);
};
const FloatingDockDesktop = ({
items,
className,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
className?: string;
}) => {
const mouseX = useMotionValue(Infinity);
return (
<motion.div
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
className={cn(
"mx-auto hidden h-16 items-end gap-4 rounded-2xl bg-gray-50 px-4 pb-3 md:flex dark:bg-neutral-900",
className
)}
>
{items.map((item) => (
<IconContainer mouseX={mouseX} key={item.title} {...item} />
))}
</motion.div>
);
};
function IconContainer({
mouseX,
title,
icon,
href,
}: {
mouseX: MotionValue;
title: string;
icon: React.ReactNode;
href: string;
}) {
const ref = useRef<HTMLDivElement>(null);
const distance = useTransform(mouseX, (val) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
const widthTransform = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
const heightTransform = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
const widthTransformIcon = useTransform(distance, [-150, 0, 150], [20, 40, 20]);
const heightTransformIcon = useTransform(distance, [-150, 0, 150], [20, 40, 20]);
const width = useSpring(widthTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const height = useSpring(heightTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const widthIcon = useSpring(widthTransformIcon, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const heightIcon = useSpring(heightTransformIcon, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const [hovered, setHovered] = useState(false);
return (
<a href={href}>
<motion.div
ref={ref}
style={{ width, height }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className="relative flex aspect-square items-center justify-center rounded-full bg-gray-200 dark:bg-neutral-800"
>
<AnimatePresence>
{hovered && (
<motion.div
initial={{ opacity: 0, y: 10, x: "-50%" }}
animate={{ opacity: 1, y: 0, x: "-50%" }}
exit={{ opacity: 0, y: 2, x: "-50%" }}
className="absolute -top-8 left-1/2 w-fit rounded-md border border-gray-200 bg-gray-100 px-2 py-0.5 text-xs whitespace-pre text-neutral-700 dark:border-neutral-900 dark:bg-neutral-800 dark:text-white"
>
{title}
</motion.div>
)}
</AnimatePresence>
<motion.div
style={{ width: widthIcon, height: heightIcon }}
className="flex items-center justify-center"
>
{icon}
</motion.div>
</motion.div>
</a>
);
}
+64
View File
@@ -0,0 +1,64 @@
"use client";
import React, { useState } from "react";
import { cn } from "@/lib/utils";
export const Card = React.memo(
({
card,
index,
hovered,
setHovered,
}: {
card: any;
index: number;
hovered: number | null;
setHovered: React.Dispatch<React.SetStateAction<number | null>>;
}) => (
<div
onMouseEnter={() => setHovered(index)}
onMouseLeave={() => setHovered(null)}
className={cn(
"relative h-60 w-full overflow-hidden rounded-lg bg-gray-100 transition-all duration-300 ease-out md:h-96 dark:bg-neutral-900",
hovered !== null && hovered !== index && "scale-[0.98] blur-sm"
)}
>
<img src={card.src} alt={card.title} className="absolute inset-0 object-cover" />
<div
className={cn(
"absolute inset-0 flex items-end bg-black/50 px-4 py-8 transition-opacity duration-300",
hovered === index ? "opacity-100" : "opacity-0"
)}
>
<div className="bg-gradient-to-b from-neutral-50 to-neutral-200 bg-clip-text text-xl font-medium text-transparent md:text-2xl">
{card.title}
</div>
</div>
</div>
)
);
Card.displayName = "Card";
type Card = {
title: string;
src: string;
};
export function FocusCards({ cards }: { cards: Card[] }) {
const [hovered, setHovered] = useState<number | null>(null);
return (
<div className="mx-auto grid w-full max-w-5xl grid-cols-1 gap-10 md:grid-cols-3 md:px-8">
{cards.map((card, index) => (
<Card
key={card.title}
card={card}
index={index}
hovered={hovered}
setHovered={setHovered}
/>
))}
</div>
);
}
File diff suppressed because one or more lines are too long
+236
View File
@@ -0,0 +1,236 @@
"use client";
import { useState, useRef, useEffect, useId, useMemo, useCallback, type ChangeEvent } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
function GooeyFilter({ filterId, blur }: { filterId: string; blur: number }) {
return (
<svg className="absolute hidden h-0 w-0" aria-hidden>
<defs>
<filter id={filterId} x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation={blur} result="blur" />
<feColorMatrix
in="blur"
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -10"
result="goo"
/>
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
</filter>
</defs>
</svg>
);
}
function SearchIcon({ layoutId }: { layoutId: string }) {
return (
<motion.svg
layoutId={layoutId}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
className="size-4 shrink-0"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</motion.svg>
);
}
const transition = {
duration: 0.4,
type: "spring" as const,
bounce: 0.25,
};
const iconBubbleVariants = {
collapsed: { scale: 0, opacity: 0 },
expanded: { scale: 1, opacity: 1 },
};
export interface GooeyInputClassNames {
root?: string;
filterWrap?: string;
buttonRow?: string;
trigger?: string;
input?: string;
bubble?: string;
bubbleSurface?: string;
}
export interface GooeyInputProps {
placeholder?: string;
className?: string;
classNames?: GooeyInputClassNames;
/** Collapsed control width in px */
collapsedWidth?: number;
/** Expanded control width in px */
expandedWidth?: number;
/** Horizontal offset when expanded (px), aligns detached bubble */
expandedOffset?: number;
/** Gaussian blur amount for the gooey SVG filter */
gooeyBlur?: number;
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
onOpenChange?: (open: boolean) => void;
disabled?: boolean;
}
export function GooeyInput({
placeholder = "Type to search...",
className,
classNames,
collapsedWidth = 115,
expandedWidth = 200,
expandedOffset = 50,
gooeyBlur = 5,
value: valueProp,
defaultValue = "",
onValueChange,
onOpenChange,
disabled = false,
}: GooeyInputProps) {
const reactId = useId();
const safeId = reactId.replace(/:/g, "");
const filterId = `gooey-filter-${safeId}`;
const iconLayoutId = `gooey-input-icon-${safeId}`;
const inputLayoutId = `gooey-input-field-${safeId}`;
const inputRef = useRef<HTMLInputElement>(null);
const prevExpandedRef = useRef(false);
const [isExpanded, setIsExpanded] = useState(false);
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
const isControlled = valueProp !== undefined;
const searchText = isControlled ? valueProp : uncontrolledValue;
const setSearchText = useCallback(
(next: string) => {
if (!isControlled) {
setUncontrolledValue(next);
}
onValueChange?.(next);
},
[isControlled, onValueChange]
);
const setExpanded = useCallback(
(next: boolean) => {
setIsExpanded(next);
onOpenChange?.(next);
},
[onOpenChange]
);
useEffect(() => {
if (isExpanded) {
inputRef.current?.focus();
} else if (prevExpandedRef.current) {
setSearchText("");
}
prevExpandedRef.current = isExpanded;
}, [isExpanded, setSearchText]);
const buttonVariants = useMemo(
() => ({
collapsed: { width: collapsedWidth, marginLeft: 0 },
expanded: { width: expandedWidth, marginLeft: expandedOffset },
}),
[collapsedWidth, expandedWidth, expandedOffset]
);
const handleExpand = useCallback(() => {
if (!disabled) setExpanded(true);
}, [disabled, setExpanded]);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setSearchText(e.target.value);
},
[setSearchText]
);
const handleBlur = useCallback(() => {
if (!searchText) setExpanded(false);
}, [searchText, setExpanded]);
const surfaceClass = "bg-foreground text-background shadow-sm ring-1 ring-border/60";
return (
<div className={cn("relative flex items-center justify-center", className, classNames?.root)}>
<GooeyFilter filterId={filterId} blur={gooeyBlur} />
<div
className={cn("relative flex h-10 items-center justify-center", classNames?.filterWrap)}
style={{ filter: `url(#${filterId})` }}
>
<motion.div
className={cn("flex h-10 items-center justify-center", classNames?.buttonRow)}
variants={buttonVariants}
initial="collapsed"
animate={isExpanded ? "expanded" : "collapsed"}
transition={transition}
>
<button
type="button"
disabled={disabled}
onClick={handleExpand}
className={cn(
"flex h-10 w-full cursor-pointer items-center justify-center gap-2 rounded-full px-4 text-sm font-medium transition-[color,box-shadow] outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50",
surfaceClass,
classNames?.trigger
)}
>
{!isExpanded ? <SearchIcon layoutId={iconLayoutId} /> : null}
<motion.input
layoutId={inputLayoutId}
ref={inputRef}
type="search"
enterKeyHint="search"
autoComplete="off"
value={searchText}
onChange={handleChange}
onBlur={handleBlur}
disabled={disabled || !isExpanded}
placeholder={placeholder}
className={cn(
"h-full min-w-0 flex-1 bg-transparent text-sm text-background outline-none",
isExpanded
? "placeholder:text-background/50 dark:placeholder:text-background/45"
: "pointer-events-none placeholder:text-background/80 dark:placeholder:text-background/70",
classNames?.input
)}
/>
</button>
</motion.div>
<motion.div
className={cn(
"absolute top-1/2 left-0 flex size-10 -translate-y-1/2 items-center justify-center",
classNames?.bubble
)}
variants={iconBubbleVariants}
initial="collapsed"
animate={isExpanded ? "expanded" : "collapsed"}
transition={transition}
>
<div
className={cn(
"flex size-10 items-center justify-center rounded-full",
surfaceClass,
classNames?.bubbleSurface
)}
>
<SearchIcon layoutId={iconLayoutId} />
</div>
</motion.div>
</div>
</div>
);
}
File diff suppressed because one or more lines are too long
+136
View File
@@ -0,0 +1,136 @@
"use client";
import { cn } from "@/lib/utils";
import { useMotionValue, motion, useMotionTemplate } from "motion/react";
import React from "react";
export const HeroHighlight = ({
children,
className,
containerClassName,
}: {
children: React.ReactNode;
className?: string;
containerClassName?: string;
}) => {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
// SVG patterns for different states and themes
const dotPatterns = {
light: {
default: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='16' height='16' fill='none'%3E%3Ccircle fill='%23d4d4d4' id='pattern-circle' cx='10' cy='10' r='2.5'%3E%3C/circle%3E%3C/svg%3E")`,
hover: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='16' height='16' fill='none'%3E%3Ccircle fill='%236366f1' id='pattern-circle' cx='10' cy='10' r='2.5'%3E%3C/circle%3E%3C/svg%3E")`,
},
dark: {
default: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='16' height='16' fill='none'%3E%3Ccircle fill='%23404040' id='pattern-circle' cx='10' cy='10' r='2.5'%3E%3C/circle%3E%3C/svg%3E")`,
hover: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='16' height='16' fill='none'%3E%3Ccircle fill='%238183f4' id='pattern-circle' cx='10' cy='10' r='2.5'%3E%3C/circle%3E%3C/svg%3E")`,
},
};
function handleMouseMove({ currentTarget, clientX, clientY }: React.MouseEvent<HTMLDivElement>) {
if (!currentTarget) return;
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top);
}
return (
<div
className={cn(
"group relative flex h-[40rem] w-full items-center justify-center bg-white dark:bg-black",
containerClassName
)}
onMouseMove={handleMouseMove}
>
<div
className="pointer-events-none absolute inset-0 dark:hidden"
style={{
backgroundImage: dotPatterns.light.default,
}}
/>
<div
className="pointer-events-none absolute inset-0 hidden dark:block"
style={{
backgroundImage: dotPatterns.dark.default,
}}
/>
<motion.div
className="pointer-events-none absolute inset-0 opacity-0 transition duration-300 group-hover:opacity-100 dark:hidden"
style={{
backgroundImage: dotPatterns.light.hover,
WebkitMaskImage: useMotionTemplate`
radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
black 0%,
transparent 100%
)
`,
maskImage: useMotionTemplate`
radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
black 0%,
transparent 100%
)
`,
}}
/>
<motion.div
className="pointer-events-none absolute inset-0 hidden opacity-0 transition duration-300 group-hover:opacity-100 dark:block"
style={{
backgroundImage: dotPatterns.dark.hover,
WebkitMaskImage: useMotionTemplate`
radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
black 0%,
transparent 100%
)
`,
maskImage: useMotionTemplate`
radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
black 0%,
transparent 100%
)
`,
}}
/>
<div className={cn("relative z-20", className)}>{children}</div>
</div>
);
};
export const Highlight = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<motion.span
initial={{
backgroundSize: "0% 100%",
}}
animate={{
backgroundSize: "100% 100%",
}}
transition={{
duration: 2,
ease: "linear",
delay: 0.5,
}}
style={{
backgroundRepeat: "no-repeat",
backgroundPosition: "left center",
display: "inline",
}}
className={cn(
`relative inline-block rounded-lg bg-gradient-to-r from-indigo-300 to-purple-300 px-1 pb-1 dark:from-indigo-500 dark:to-purple-500`,
className
)}
>
{children}
</motion.span>
);
};
+120
View File
@@ -0,0 +1,120 @@
"use client";
import React from "react";
import { motion, useScroll, useTransform, useSpring, MotionValue } from "motion/react";
export const HeroParallax = ({
products,
}: {
products: {
title: string;
link: string;
thumbnail: string;
}[];
}) => {
const firstRow = products.slice(0, 5);
const secondRow = products.slice(5, 10);
const thirdRow = products.slice(10, 15);
const ref = React.useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const springConfig = { stiffness: 300, damping: 30, bounce: 100 };
const translateX = useSpring(useTransform(scrollYProgress, [0, 1], [0, 1000]), springConfig);
const translateXReverse = useSpring(
useTransform(scrollYProgress, [0, 1], [0, -1000]),
springConfig
);
const rotateX = useSpring(useTransform(scrollYProgress, [0, 0.2], [15, 0]), springConfig);
const opacity = useSpring(useTransform(scrollYProgress, [0, 0.2], [0.2, 1]), springConfig);
const rotateZ = useSpring(useTransform(scrollYProgress, [0, 0.2], [20, 0]), springConfig);
const translateY = useSpring(useTransform(scrollYProgress, [0, 0.2], [-700, 500]), springConfig);
return (
<div
ref={ref}
className="relative flex h-[300vh] flex-col self-auto overflow-hidden py-40 antialiased [perspective:1000px] [transform-style:preserve-3d]"
>
<Header />
<motion.div
style={{
rotateX,
rotateZ,
translateY,
opacity,
}}
className=""
>
<motion.div className="mb-20 flex flex-row-reverse space-x-20 space-x-reverse">
{firstRow.map((product) => (
<ProductCard product={product} translate={translateX} key={product.title} />
))}
</motion.div>
<motion.div className="mb-20 flex flex-row space-x-20">
{secondRow.map((product) => (
<ProductCard product={product} translate={translateXReverse} key={product.title} />
))}
</motion.div>
<motion.div className="flex flex-row-reverse space-x-20 space-x-reverse">
{thirdRow.map((product) => (
<ProductCard product={product} translate={translateX} key={product.title} />
))}
</motion.div>
</motion.div>
</div>
);
};
export const Header = () => {
return (
<div className="relative top-0 left-0 mx-auto w-full max-w-7xl px-4 py-20 md:py-40">
<h1 className="text-2xl font-bold md:text-7xl dark:text-white">
The Ultimate <br /> development studio
</h1>
<p className="mt-8 max-w-2xl text-base md:text-xl dark:text-neutral-200">
We build beautiful products with the latest technologies and frameworks. We are a team of
passionate developers and designers that love to build amazing products.
</p>
</div>
);
};
export const ProductCard = ({
product,
translate,
}: {
product: {
title: string;
link: string;
thumbnail: string;
};
translate: MotionValue<number>;
}) => {
return (
<motion.div
style={{
x: translate,
}}
whileHover={{
y: -20,
}}
key={product.title}
className="group/product relative h-96 w-[30rem] shrink-0"
>
<a href={product.link} className="block group-hover/product:shadow-2xl">
<img
src={product.thumbnail}
height="600"
width="600"
className="absolute inset-0 h-full w-full object-cover object-left-top"
alt={product.title}
/>
</a>
<div className="pointer-events-none absolute inset-0 h-full w-full bg-black opacity-0 group-hover/product:opacity-80"></div>
<h2 className="absolute bottom-4 left-4 text-white opacity-0 group-hover/product:opacity-100">
{product.title}
</h2>
</motion.div>
);
};
+115
View File
@@ -0,0 +1,115 @@
"use client";
import { cn } from "@/lib/utils";
import React, { useEffect, useState } from "react";
export const InfiniteMovingCards = ({
items,
direction = "left",
speed = "fast",
pauseOnHover = true,
className,
}: {
items: {
quote: string;
name: string;
title: string;
}[];
direction?: "left" | "right";
speed?: "fast" | "normal" | "slow";
pauseOnHover?: boolean;
className?: string;
}) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const scrollerRef = React.useRef<HTMLUListElement>(null);
useEffect(() => {
addAnimation();
}, []);
const [start, setStart] = useState(false);
function addAnimation() {
if (containerRef.current && scrollerRef.current) {
const scrollerContent = Array.from(scrollerRef.current.children);
scrollerContent.forEach((item) => {
const duplicatedItem = item.cloneNode(true);
if (scrollerRef.current) {
scrollerRef.current.appendChild(duplicatedItem);
}
});
getDirection();
getSpeed();
setStart(true);
}
}
useEffect(() => {
setStart(false);
}, []);
const getDirection = () => {
if (containerRef.current) {
if (direction === "left") {
containerRef.current.style.setProperty("--animation-direction", "forwards");
} else {
containerRef.current.style.setProperty("--animation-direction", "reverse");
}
}
};
const getSpeed = () => {
if (containerRef.current) {
if (speed === "fast") {
containerRef.current.style.setProperty("--animation-duration", "20s");
} else if (speed === "normal") {
containerRef.current.style.setProperty("--animation-duration", "40s");
} else {
containerRef.current.style.setProperty("--animation-duration", "80s");
}
}
};
return (
<div
ref={containerRef}
className={cn(
"scroller relative z-20 max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]",
className
)}
>
<ul
ref={scrollerRef}
className={cn(
"flex w-max min-w-full shrink-0 flex-nowrap gap-4 py-4",
start && "animate-scroll",
pauseOnHover && "hover:[animation-play-state:paused]"
)}
>
{items.map((item) => (
<li
className="relative w-[350px] max-w-full shrink-0 rounded-2xl border border-b-0 border-zinc-200 bg-[linear-gradient(180deg,#fafafa,#f5f5f5)] px-8 py-6 md:w-[450px] dark:border-zinc-700 dark:bg-[linear-gradient(180deg,#27272a,#18181b)]"
key={item.name}
>
<blockquote>
<div
aria-hidden="true"
className="user-select-none pointer-events-none absolute -top-0.5 -left-0.5 -z-1 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]"
></div>
<span className="relative z-20 text-sm leading-[1.6] font-normal text-neutral-800 dark:text-gray-100">
{item.quote}
</span>
<div className="relative z-20 mt-6 flex flex-row items-center">
<span className="flex flex-col gap-1">
<span className="text-sm leading-[1.6] font-normal text-neutral-500 dark:text-gray-400">
{item.name}
</span>
<span className="text-sm leading-[1.6] font-normal text-neutral-500 dark:text-gray-400">
{item.title}
</span>
</span>
</div>
</blockquote>
</li>
))}
</ul>
</div>
);
};
+63
View File
@@ -0,0 +1,63 @@
"use client";
import React, { useRef, useState } from "react";
import { motion } from "motion/react";
export const MagneticButton = ({
children,
strength = 0.8,
maxDistance = 100,
}: {
children: React.ReactNode;
strength?: number;
maxDistance?: number;
}) => {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!ref.current) return;
const { width, height, left, top } = ref.current.getBoundingClientRect();
const { clientX, clientY } = e;
let x = (clientX - (left + width / 2)) * strength;
let y = (clientY - (top + height / 2)) * strength;
const distance = Math.hypot(x, y);
if (distance > maxDistance) {
const scale = maxDistance / distance;
x *= scale;
y *= scale;
}
setPosition({ x, y });
};
const handleMouseLeave = () => {
setPosition({ x: 0, y: 0 });
};
const hasMoved = position.x !== 0 || position.y !== 0;
return (
<div
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="cursor-pointer rounded-lg border border-dashed transition-colors duration-150 [--show-color:var(--color-blue-500)]"
style={{
borderColor: hasMoved ? "var(--show-color)" : "transparent",
backgroundColor: hasMoved
? "color-mix(in srgb,var(--show-color) 20%, transparent)"
: "transparent",
}}
>
<motion.div
ref={ref}
animate={{ x: position.x, y: position.y }}
transition={{ type: "spring", stiffness: 150, damping: 25, mass: 0.1 }}
>
{children}
</motion.div>
</div>
);
};
+33
View File
@@ -0,0 +1,33 @@
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
export const Meteors = ({ number, className }: { number?: number; className?: string }) => {
const meteors = new Array(number || 20).fill(true);
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }}>
{meteors.map((_, idx) => {
const meteorCount = number || 20;
// Calculate position to evenly distribute meteors across container width
const position = idx * (800 / meteorCount) - 400; // Spread across 800px range, centered
return (
<span
key={"meteor" + idx}
className={cn(
"animate-meteor-effect absolute h-0.5 w-0.5 rotate-[45deg] rounded-[9999px] bg-slate-500 shadow-[0_0_0_1px_#ffffff10]",
"before:absolute before:top-1/2 before:h-[1px] before:w-[50px] before:-translate-y-[50%] before:transform before:bg-gradient-to-r before:from-[#64748b] before:to-transparent before:content-['']",
className
)}
style={{
top: "-40px", // Start above the container
left: position + "px",
animationDelay: Math.random() * 5 + "s", // Random delay between 0-5s
animationDuration: Math.floor(Math.random() * (10 - 5) + 5) + "s", // Keep some randomness in duration
}}
></span>
);
})}
</motion.div>
);
};
+123
View File
@@ -0,0 +1,123 @@
"use client";
import React from "react";
import {
motion,
useAnimationFrame,
useMotionTemplate,
useMotionValue,
useTransform,
} from "motion/react";
import { useRef } from "react";
import { cn } from "@/lib/utils";
export function Button({
borderRadius = "1.75rem",
children,
as: Component = "button",
containerClassName,
borderClassName,
duration,
className,
...otherProps
}: {
borderRadius?: string;
children: React.ReactNode;
as?: any;
containerClassName?: string;
borderClassName?: string;
duration?: number;
className?: string;
[key: string]: any;
}) {
return (
<Component
className={cn(
"relative h-16 w-40 overflow-hidden bg-transparent p-[1px] text-xl",
containerClassName
)}
style={{
borderRadius: borderRadius,
}}
{...otherProps}
>
<div className="absolute inset-0" style={{ borderRadius: `calc(${borderRadius} * 0.96)` }}>
<MovingBorder duration={duration} rx="30%" ry="30%">
<div
className={cn(
"h-20 w-20 bg-[radial-gradient(#0ea5e9_40%,transparent_60%)] opacity-[0.8]",
borderClassName
)}
/>
</MovingBorder>
</div>
<div
className={cn(
"relative flex h-full w-full items-center justify-center border border-slate-800 bg-slate-900/[0.8] text-sm text-white antialiased backdrop-blur-xl",
className
)}
style={{
borderRadius: `calc(${borderRadius} * 0.96)`,
}}
>
{children}
</div>
</Component>
);
}
export const MovingBorder = ({
children,
duration = 3000,
rx,
ry,
...otherProps
}: {
children: React.ReactNode;
duration?: number;
rx?: string;
ry?: string;
[key: string]: any;
}) => {
const pathRef = useRef<SVGRectElement>(null);
const progress = useMotionValue<number>(0);
useAnimationFrame((time) => {
const length = pathRef.current?.getTotalLength();
if (length) {
const pxPerMillisecond = length / duration;
progress.set((time * pxPerMillisecond) % length);
}
});
const x = useTransform(progress, (val) => pathRef.current?.getPointAtLength(val).x);
const y = useTransform(progress, (val) => pathRef.current?.getPointAtLength(val).y);
const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`;
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
className="absolute h-full w-full"
width="100%"
height="100%"
{...otherProps}
>
<rect fill="none" width="100%" height="100%" rx={rx} ry={ry} ref={pathRef} />
</svg>
<motion.div
style={{
position: "absolute",
top: 0,
left: 0,
display: "inline-block",
transform,
}}
>
{children}
</motion.div>
</>
);
};
+143
View File
@@ -0,0 +1,143 @@
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import { useState, useEffect } from "react";
const CheckIcon = ({ className }: { className?: string }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={cn("h-6 w-6", className)}
>
<path d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
);
};
const CheckFilled = ({ className }: { className?: string }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className={cn("h-6 w-6", className)}
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clipRule="evenodd"
/>
</svg>
);
};
type LoadingState = {
text: string;
};
const LoaderCore = ({
loadingStates,
value = 0,
}: {
loadingStates: LoadingState[];
value?: number;
}) => {
return (
<div className="relative mx-auto mt-40 flex max-w-xl flex-col justify-start">
{loadingStates.map((loadingState, index) => {
const distance = Math.abs(index - value);
const opacity = Math.max(1 - distance * 0.2, 0); // Minimum opacity is 0, keep it 0.2 if you're sane.
return (
<motion.div
key={index}
className={cn("mb-4 flex gap-2 text-left")}
initial={{ opacity: 0, y: -(value * 40) }}
animate={{ opacity: opacity, y: -(value * 40) }}
transition={{ duration: 0.5 }}
>
<div>
{index > value && <CheckIcon className="text-black dark:text-white" />}
{index <= value && (
<CheckFilled
className={cn(
"text-black dark:text-white",
value === index && "text-black opacity-100 dark:text-lime-500"
)}
/>
)}
</div>
<span
className={cn(
"text-black dark:text-white",
value === index && "text-black opacity-100 dark:text-lime-500"
)}
>
{loadingState.text}
</span>
</motion.div>
);
})}
</div>
);
};
export const MultiStepLoader = ({
loadingStates,
loading,
duration = 2000,
loop = true,
}: {
loadingStates: LoadingState[];
loading?: boolean;
duration?: number;
loop?: boolean;
}) => {
const [currentState, setCurrentState] = useState(0);
useEffect(() => {
if (!loading) {
setCurrentState(0);
return;
}
const timeout = setTimeout(() => {
setCurrentState((prevState) =>
loop
? prevState === loadingStates.length - 1
? 0
: prevState + 1
: Math.min(prevState + 1, loadingStates.length - 1)
);
}, duration);
return () => clearTimeout(timeout);
}, [currentState, loading, loop, loadingStates.length, duration]);
return (
<AnimatePresence mode="wait">
{loading && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
}}
className="fixed inset-0 z-[100] flex h-full w-full items-center justify-center backdrop-blur-2xl"
>
<div className="relative h-96">
<LoaderCore value={currentState} loadingStates={loadingStates} />
</div>
<div className="absolute inset-x-0 bottom-0 z-20 h-full bg-white bg-gradient-to-t [mask-image:radial-gradient(900px_at_center,transparent_30%,white)] dark:bg-black" />
</motion.div>
)}
</AnimatePresence>
);
};
+290
View File
@@ -0,0 +1,290 @@
"use client";
import React, { useEffect, useId, useRef, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/lib/utils";
export type NotchOption = {
/** Stable identifier passed back in callbacks. */
id: string;
/** What renders for this option. Can be text or any node. */
label: React.ReactNode;
/** Optional leading node (icon, swatch, etc.) shown before the label. */
icon?: React.ReactNode;
};
export type NotchItem = {
/** Stable identifier for the group. */
id: string;
/** Trigger label shown in the bar. */
label: React.ReactNode;
/** Optional leading icon for the trigger. */
icon?: React.ReactNode;
/** The choices revealed when the group is opened. */
options: NotchOption[];
/** Uncontrolled initial selected option id. */
defaultValue?: string;
/** Controlled selected option id. */
value?: string;
/** Show the selected value next to the trigger label. Overrides `showSelectedValue`. */
showValue?: boolean;
/** Fires with the selected option whenever it changes. */
onChange?: (optionId: string, option: NotchOption) => void;
};
export interface NotchProps {
/** The groups shown inside the notch. Pass one or many. */
items: NotchItem[];
/** Pin the notch to the top or bottom of the viewport. */
position?: "top" | "bottom";
/** Horizontal alignment of the floating notch. */
align?: "start" | "center" | "end";
/** Fired for any group change, in addition to the per-item callback. */
onItemChange?: (itemId: string, optionId: string, option: NotchOption) => void;
/** Close the panel after selecting an option. */
closeOnSelect?: boolean;
/** Show each group's selected value next to its trigger label. */
showSelectedValue?: boolean;
/** Render dotted dividers between groups. */
showDividers?: boolean;
/** Highlight color for the selected option. Any CSS color or variable. */
accentColor?: string;
/** Distance from the pinned edge, in pixels. */
offset?: number;
/** Play the entrance animation on mount. */
reveal?: boolean;
/** Classes applied to the floating shell. */
className?: string;
/** Classes applied to every trigger. */
itemClassName?: string;
/** Classes applied to the options panel. */
panelClassName?: string;
}
const SHELL_SPRING = { type: "spring" as const, stiffness: 380, damping: 34 };
const LIST_VARIANTS = {
hidden: {},
visible: { transition: { staggerChildren: 0.045, delayChildren: 0.08 } },
};
const OPTION_VARIANTS = {
hidden: { opacity: 0, y: -10, filter: "blur(4px)" },
visible: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { type: "spring" as const, stiffness: 420, damping: 30 },
},
};
function NotchDivider() {
return (
<span
aria-hidden
className="mx-0.5 h-5 w-px shrink-0 self-center"
style={{
backgroundImage:
"repeating-linear-gradient(180deg, rgba(255,255,255,0.35) 0px, rgba(255,255,255,0.35) 1px, transparent 1px, transparent 5px)",
backgroundSize: "1px 4px",
backgroundRepeat: "repeat-y",
}}
/>
);
}
export const Notch = ({
items,
position = "bottom",
align = "center",
onItemChange,
closeOnSelect = true,
showSelectedValue = true,
showDividers = true,
accentColor = "var(--color-blue-500, #3b82f6)",
offset = 16,
reveal = true,
className,
itemClassName,
panelClassName,
}: NotchProps) => {
const shellRef = useRef<HTMLDivElement>(null);
const shellLayoutId = useId();
const [openItemId, setOpenItemId] = useState<string | null>(null);
const [internalSelected, setInternalSelected] = useState<Record<string, string>>(() => {
const map: Record<string, string> = {};
for (const item of items) {
if (item.value === undefined) {
map[item.id] = item.defaultValue ?? item.options[0]?.id ?? "";
}
}
return map;
});
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") setOpenItemId(null);
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
useEffect(() => {
if (!openItemId) return;
function onPointerDown(e: PointerEvent) {
if (!shellRef.current?.contains(e.target as Node)) setOpenItemId(null);
}
document.addEventListener("pointerdown", onPointerDown);
return () => document.removeEventListener("pointerdown", onPointerDown);
}, [openItemId]);
const getSelectedId = (item: NotchItem) =>
item.value ?? internalSelected[item.id] ?? item.options[0]?.id;
const getSelectedOption = (item: NotchItem) =>
item.options.find((o) => o.id === getSelectedId(item));
const handleSelect = (item: NotchItem, option: NotchOption) => {
if (item.value === undefined) {
setInternalSelected((prev) => ({ ...prev, [item.id]: option.id }));
}
item.onChange?.(option.id, option);
onItemChange?.(item.id, option.id, option);
if (closeOnSelect) setOpenItemId(null);
};
const alignClass =
align === "start" ? "justify-start" : align === "end" ? "justify-end" : "justify-center";
const edgeOffset = (offset + 20) * (position === "top" ? -1 : 1);
const openItem = items.find((i) => i.id === openItemId) ?? null;
const optionsPanel = openItem ? (
<motion.div
key={openItem.id}
role="listbox"
aria-label={typeof openItem.label === "string" ? openItem.label : openItem.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className={cn("w-fit", panelClassName)}
>
<motion.div
className="flex flex-col gap-1.5 p-2"
variants={LIST_VARIANTS}
initial="hidden"
animate="visible"
>
{openItem.options.map((option) => {
const active = option.id === getSelectedId(openItem);
return (
<motion.button
key={option.id}
role="option"
aria-selected={active}
type="button"
variants={OPTION_VARIANTS}
onClick={() => handleSelect(openItem, option)}
className={cn(
"flex w-full items-center justify-between gap-6 rounded-md px-3 py-2 text-left text-xs font-medium whitespace-nowrap transition-colors",
active ? "text-white" : "text-neutral-300 hover:bg-white/5 hover:text-white"
)}
style={
active
? {
background: `color-mix(in oklab, ${accentColor} 85%, transparent)`,
boxShadow: `inset 0 0 0 1px color-mix(in oklab, ${accentColor} 40%, transparent)`,
}
: undefined
}
>
<span className="flex items-center gap-2.5">
{option.icon ? (
<span className="flex shrink-0 items-center justify-center">{option.icon}</span>
) : null}
<span>{option.label}</span>
</span>
{active ? (
<span
className="size-1.5 shrink-0 rounded-full"
style={{ background: accentColor }}
/>
) : null}
</motion.button>
);
})}
</motion.div>
</motion.div>
) : (
<motion.div
key="__notch-triggers"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="flex w-fit items-center gap-1 p-1"
>
{items.map((item, index) => {
const selected = getSelectedOption(item);
const isLast = index === items.length - 1;
return (
<React.Fragment key={item.id}>
<button
type="button"
aria-haspopup="listbox"
aria-expanded={false}
onClick={() => setOpenItemId(item.id)}
className={cn(
"group flex items-center gap-2 rounded-full px-3 py-2 text-xs font-medium whitespace-nowrap text-neutral-300 transition-colors hover:bg-white/6 hover:text-white",
itemClassName
)}
>
{item.icon ? (
<span className="flex shrink-0 items-center justify-center">{item.icon}</span>
) : null}
<span className="text-neutral-100">{item.label}</span>
{(item.showValue ?? showSelectedValue) && selected ? (
<span className="text-neutral-400">{selected.label}</span>
) : null}
</button>
{showDividers && !isLast ? <NotchDivider /> : null}
</React.Fragment>
);
})}
</motion.div>
);
return (
<div
className={cn(
"pointer-events-none fixed inset-x-0 z-100 flex translate-z-0 px-4",
position === "top" ? "top-0" : "bottom-0",
alignClass
)}
style={
position === "top"
? { paddingTop: `max(${offset}px, env(safe-area-inset-top))` }
: { paddingBottom: `max(${offset}px, env(safe-area-inset-bottom))` }
}
>
<motion.div
ref={shellRef}
layoutId={shellLayoutId}
layout
initial={reveal ? { opacity: 0, y: edgeOffset, filter: "blur(6px)" } : false}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={SHELL_SPRING}
className={cn(
"pointer-events-auto flex w-fit flex-col overflow-hidden rounded-xl border border-white/10 bg-neutral-950/95 shadow-[0_12px_40px_-8px_rgba(0,0,0,0.55)] ring-1 ring-neutral-800 backdrop-blur-2xl ring-inset",
className
)}
>
<AnimatePresence mode="popLayout" initial={false}>
{optionsPanel}
</AnimatePresence>
</motion.div>
</div>
);
};
+76
View File
@@ -0,0 +1,76 @@
"use client";
import { useScroll, useTransform } from "motion/react";
import { useRef } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const ParallaxScroll = ({ images, className }: { images: string[]; className?: string }) => {
const gridRef = useRef<any>(null);
const { scrollYProgress } = useScroll({
container: gridRef, // remove this if your container is not fixed height
offset: ["start start", "end start"], // remove this if your container is not fixed height
});
const translateFirst = useTransform(scrollYProgress, [0, 1], [0, -200]);
const translateSecond = useTransform(scrollYProgress, [0, 1], [0, 200]);
const translateThird = useTransform(scrollYProgress, [0, 1], [0, -200]);
const third = Math.ceil(images.length / 3);
const firstPart = images.slice(0, third);
const secondPart = images.slice(third, 2 * third);
const thirdPart = images.slice(2 * third);
return (
<div className={cn("h-[40rem] w-full items-start overflow-y-auto", className)} ref={gridRef}>
<div
className="mx-auto grid max-w-5xl grid-cols-1 items-start gap-10 px-10 py-40 md:grid-cols-2 lg:grid-cols-3"
ref={gridRef}
>
<div className="grid gap-10">
{firstPart.map((el, idx) => (
<motion.div
style={{ y: translateFirst }} // Apply the translateY motion value here
key={"grid-1" + idx}
>
<img
src={el}
className="!m-0 h-80 w-full gap-10 rounded-lg object-cover object-left-top !p-0"
height="400"
width="400"
alt="thumbnail"
/>
</motion.div>
))}
</div>
<div className="grid gap-10">
{secondPart.map((el, idx) => (
<motion.div style={{ y: translateSecond }} key={"grid-2" + idx}>
<img
src={el}
className="!m-0 h-80 w-full gap-10 rounded-lg object-cover object-left-top !p-0"
height="400"
width="400"
alt="thumbnail"
/>
</motion.div>
))}
</div>
<div className="grid gap-10">
{thirdPart.map((el, idx) => (
<motion.div style={{ y: translateThird }} key={"grid-3" + idx}>
<img
src={el}
className="!m-0 h-80 w-full gap-10 rounded-lg object-cover object-left-top !p-0"
height="400"
width="400"
alt="thumbnail"
/>
</motion.div>
))}
</div>
</div>
</div>
);
};
@@ -0,0 +1,267 @@
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
export function PlaceholdersAndVanishInput({
placeholders,
onChange,
onSubmit,
}: {
placeholders: string[];
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
}) {
const [currentPlaceholder, setCurrentPlaceholder] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const startAnimation = () => {
intervalRef.current = setInterval(() => {
setCurrentPlaceholder((prev) => (prev + 1) % placeholders.length);
}, 3000);
};
const handleVisibilityChange = () => {
if (document.visibilityState !== "visible" && intervalRef.current) {
clearInterval(intervalRef.current); // Clear the interval when the tab is not visible
intervalRef.current = null;
} else if (document.visibilityState === "visible") {
startAnimation(); // Restart the interval when the tab becomes visible
}
};
useEffect(() => {
startAnimation();
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [placeholders]);
const canvasRef = useRef<HTMLCanvasElement>(null);
const newDataRef = useRef<any[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState("");
const [animating, setAnimating] = useState(false);
const draw = useCallback(() => {
if (!inputRef.current) return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
canvas.width = 800;
canvas.height = 800;
ctx.clearRect(0, 0, 800, 800);
const computedStyles = getComputedStyle(inputRef.current);
const fontSize = parseFloat(computedStyles.getPropertyValue("font-size"));
ctx.font = `${fontSize * 2}px ${computedStyles.fontFamily}`;
ctx.fillStyle = "#FFF";
ctx.fillText(value, 16, 40);
const imageData = ctx.getImageData(0, 0, 800, 800);
const pixelData = imageData.data;
const newData: any[] = [];
for (let t = 0; t < 800; t++) {
const i = 4 * t * 800;
for (let n = 0; n < 800; n++) {
const e = i + 4 * n;
if (pixelData[e] !== 0 && pixelData[e + 1] !== 0 && pixelData[e + 2] !== 0) {
newData.push({
x: n,
y: t,
color: [pixelData[e], pixelData[e + 1], pixelData[e + 2], pixelData[e + 3]],
});
}
}
}
newDataRef.current = newData.map(({ x, y, color }) => ({
x,
y,
r: 1,
color: `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`,
}));
}, [value]);
useEffect(() => {
draw();
}, [value, draw]);
const animate = (start: number) => {
const animateFrame = (pos: number = 0) => {
requestAnimationFrame(() => {
const newArr = [];
for (let i = 0; i < newDataRef.current.length; i++) {
const current = newDataRef.current[i];
if (current.x < pos) {
newArr.push(current);
} else {
if (current.r <= 0) {
current.r = 0;
continue;
}
current.x += Math.random() > 0.5 ? 1 : -1;
current.y += Math.random() > 0.5 ? 1 : -1;
current.r -= 0.05 * Math.random();
newArr.push(current);
}
}
newDataRef.current = newArr;
const ctx = canvasRef.current?.getContext("2d");
if (ctx) {
ctx.clearRect(pos, 0, 800, 800);
newDataRef.current.forEach((t) => {
const { x: n, y: i, r: s, color: color } = t;
if (n > pos) {
ctx.beginPath();
ctx.rect(n, i, s, s);
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.stroke();
}
});
}
if (newDataRef.current.length > 0) {
animateFrame(pos - 8);
} else {
setValue("");
setAnimating(false);
}
});
};
animateFrame(start);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !animating) {
vanishAndSubmit();
}
};
const vanishAndSubmit = () => {
setAnimating(true);
draw();
const value = inputRef.current?.value || "";
if (value && inputRef.current) {
const maxX = newDataRef.current.reduce(
(prev, current) => (current.x > prev ? current.x : prev),
0
);
animate(maxX);
}
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
vanishAndSubmit();
onSubmit && onSubmit(e);
};
return (
<form
className={cn(
"relative mx-auto h-12 w-full max-w-xl overflow-hidden rounded-full bg-white shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),_0px_1px_0px_0px_rgba(25,28,33,0.02),_0px_0px_0px_1px_rgba(25,28,33,0.08)] transition duration-200 dark:bg-zinc-800",
value && "bg-gray-50"
)}
onSubmit={handleSubmit}
>
<canvas
className={cn(
"pointer-events-none absolute top-[20%] left-2 origin-top-left scale-50 transform pr-20 text-base invert filter sm:left-8 dark:invert-0",
!animating ? "opacity-0" : "opacity-100"
)}
ref={canvasRef}
/>
<input
onChange={(e) => {
if (!animating) {
setValue(e.target.value);
onChange && onChange(e);
}
}}
onKeyDown={handleKeyDown}
ref={inputRef}
value={value}
type="text"
className={cn(
"relative z-50 h-full w-full rounded-full border-none bg-transparent pr-20 pl-4 text-sm text-black focus:ring-0 focus:outline-none sm:pl-10 sm:text-base dark:text-white",
animating && "text-transparent dark:text-transparent"
)}
/>
<button
disabled={!value}
type="submit"
className="absolute top-1/2 right-2 z-50 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-black transition duration-200 disabled:bg-gray-100 dark:bg-zinc-900 dark:disabled:bg-zinc-800"
>
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-gray-300"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<motion.path
d="M5 12l14 0"
initial={{
strokeDasharray: "50%",
strokeDashoffset: "50%",
}}
animate={{
strokeDashoffset: value ? 0 : "50%",
}}
transition={{
duration: 0.3,
ease: "linear",
}}
/>
<path d="M13 18l6 -6" />
<path d="M13 6l6 6" />
</motion.svg>
</button>
<div className="pointer-events-none absolute inset-0 flex items-center rounded-full">
<AnimatePresence mode="wait">
{!value && (
<motion.p
initial={{
y: 5,
opacity: 0,
}}
key={`current-placeholder-${currentPlaceholder}`}
animate={{
y: 0,
opacity: 1,
}}
exit={{
y: -15,
opacity: 0,
}}
transition={{
duration: 0.3,
ease: "linear",
}}
className="w-[calc(100%-2rem)] truncate pl-4 text-left text-sm font-normal text-neutral-500 sm:pl-12 sm:text-base dark:text-zinc-500"
>
{placeholders[currentPlaceholder]}
</motion.p>
)}
</AnimatePresence>
</div>
</form>
);
}
+426
View File
@@ -0,0 +1,426 @@
"use client";
import { useId } from "react";
import { useEffect, useState } from "react";
import Particles, { initParticlesEngine } from "@tsparticles/react";
import type { Container, SingleOrMultiple } from "@tsparticles/engine";
import { loadSlim } from "@tsparticles/slim";
import { cn } from "@/lib/utils";
import { motion, useAnimation } from "motion/react";
type ParticlesProps = {
id?: string;
className?: string;
background?: string;
particleSize?: number;
minSize?: number;
maxSize?: number;
speed?: number;
particleColor?: string;
particleDensity?: number;
};
export const SparklesCore = (props: ParticlesProps) => {
const { id, className, background, minSize, maxSize, speed, particleColor, particleDensity } =
props;
const [init, setInit] = useState(false);
useEffect(() => {
initParticlesEngine(async (engine) => {
await loadSlim(engine);
}).then(() => {
setInit(true);
});
}, []);
const controls = useAnimation();
const particlesLoaded = async (container?: Container) => {
if (container) {
controls.start({
opacity: 1,
transition: {
duration: 1,
},
});
}
};
const generatedId = useId();
return (
<motion.div animate={controls} className={cn("opacity-0", className)}>
{init && (
<Particles
id={id || generatedId}
className={cn("h-full w-full")}
particlesLoaded={particlesLoaded}
options={{
background: {
color: {
value: background || "#0d47a1",
},
},
fullScreen: {
enable: false,
zIndex: 1,
},
fpsLimit: 120,
interactivity: {
events: {
onClick: {
enable: true,
mode: "push",
},
onHover: {
enable: false,
mode: "repulse",
},
resize: true as any,
},
modes: {
push: {
quantity: 4,
},
repulse: {
distance: 200,
duration: 0.4,
},
},
},
particles: {
bounce: {
horizontal: {
value: 1,
},
vertical: {
value: 1,
},
},
collisions: {
absorb: {
speed: 2,
},
bounce: {
horizontal: {
value: 1,
},
vertical: {
value: 1,
},
},
enable: false,
maxSpeed: 50,
mode: "bounce",
overlap: {
enable: true,
retries: 0,
},
},
color: {
value: particleColor || "#ffffff",
animation: {
h: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: true,
offset: 0,
},
s: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: true,
offset: 0,
},
l: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: true,
offset: 0,
},
},
},
effect: {
close: true,
fill: true,
options: {},
type: {} as SingleOrMultiple<string> | undefined,
},
groups: {},
move: {
angle: {
offset: 0,
value: 90,
},
attract: {
distance: 200,
enable: false,
rotate: {
x: 3000,
y: 3000,
},
},
center: {
x: 50,
y: 50,
mode: "percent",
radius: 0,
},
decay: 0,
distance: {},
direction: "none",
drift: 0,
enable: true,
gravity: {
acceleration: 9.81,
enable: false,
inverse: false,
maxSpeed: 50,
},
path: {
clamp: true,
delay: {
value: 0,
},
enable: false,
options: {},
},
outModes: {
default: "out",
},
random: false,
size: false,
speed: {
min: 0.1,
max: 1,
},
spin: {
acceleration: 0,
enable: false,
},
straight: false,
trail: {
enable: false,
length: 10,
fill: {},
},
vibrate: false,
warp: false,
},
number: {
density: {
enable: true,
width: 400,
height: 400,
},
limit: {
mode: "delete",
value: 0,
},
value: particleDensity || 120,
},
opacity: {
value: {
min: 0.1,
max: 1,
},
animation: {
count: 0,
enable: true,
speed: speed || 4,
decay: 0,
delay: 0,
sync: false,
mode: "auto",
startValue: "random",
destroy: "none",
},
},
reduceDuplicates: false,
shadow: {
blur: 0,
color: {
value: "#000",
},
enable: false,
offset: {
x: 0,
y: 0,
},
},
shape: {
close: true,
fill: true,
options: {},
type: "circle",
},
size: {
value: {
min: minSize || 1,
max: maxSize || 3,
},
animation: {
count: 0,
enable: false,
speed: 5,
decay: 0,
delay: 0,
sync: false,
mode: "auto",
startValue: "random",
destroy: "none",
},
},
stroke: {
width: 0,
},
zIndex: {
value: 0,
opacityRate: 1,
sizeRate: 1,
velocityRate: 1,
},
destroy: {
bounds: {},
mode: "none",
split: {
count: 1,
factor: {
value: 3,
},
rate: {
value: {
min: 4,
max: 9,
},
},
sizeOffset: true,
},
},
roll: {
darken: {
enable: false,
value: 0,
},
enable: false,
enlighten: {
enable: false,
value: 0,
},
mode: "vertical",
speed: 25,
},
tilt: {
value: 0,
animation: {
enable: false,
speed: 0,
decay: 0,
sync: false,
},
direction: "clockwise",
enable: false,
},
twinkle: {
lines: {
enable: false,
frequency: 0.05,
opacity: 1,
},
particles: {
enable: false,
frequency: 0.05,
opacity: 1,
},
},
wobble: {
distance: 5,
enable: false,
speed: {
angle: 50,
move: 10,
},
},
life: {
count: 0,
delay: {
value: 0,
sync: false,
},
duration: {
value: 0,
sync: false,
},
},
rotate: {
value: 0,
animation: {
enable: false,
speed: 0,
decay: 0,
sync: false,
},
direction: "clockwise",
path: false,
},
orbit: {
animation: {
count: 0,
enable: false,
speed: 1,
decay: 0,
delay: 0,
sync: false,
},
enable: false,
opacity: 1,
rotation: {
value: 45,
},
width: 1,
},
links: {
blink: false,
color: {
value: "#fff",
},
consent: false,
distance: 100,
enable: false,
frequency: 1,
opacity: 1,
shadow: {
blur: 5,
color: {
value: "#000",
},
enable: false,
},
triangles: {
enable: false,
frequency: 1,
},
width: 1,
warp: false,
},
repulse: {
value: 0,
enabled: false,
distance: 1,
duration: 1,
factor: 1,
speed: 1,
},
},
detectRetina: true,
}}
/>
)}
</motion.div>
);
};
+55
View File
@@ -0,0 +1,55 @@
import { cn } from "@/lib/utils";
type SpotlightProps = {
className?: string;
fill?: string;
};
export const Spotlight = ({ className, fill }: SpotlightProps) => {
return (
<svg
className={cn(
"animate-spotlight pointer-events-none absolute z-[1] h-[169%] w-[138%] opacity-0 lg:w-[84%]",
className
)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 3787 2842"
fill="none"
>
<g filter="url(#filter)">
<ellipse
cx="1924.71"
cy="273.501"
rx="1924.71"
ry="273.501"
transform="matrix(-0.822377 -0.568943 -0.568943 0.822377 3631.88 2291.09)"
fill={fill || "white"}
fillOpacity="0.21"
></ellipse>
</g>
<defs>
<filter
id="filter"
x="0.860352"
y="0.838989"
width="3785.16"
height="2840.26"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix"></feFlood>
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
></feBlend>
<feGaussianBlur
stdDeviation="151"
result="effect1_foregroundBlur_1065_8"
></feGaussianBlur>
</filter>
</defs>
</svg>
);
};
+104
View File
@@ -0,0 +1,104 @@
"use client";
import React, { useId } from "react";
import { motion, useTime, useTransform } from "motion/react";
import { cn } from "@/lib/utils";
export interface SquigglyTextProps {
/**
* The text (or any node) to wrap with the squiggly effect.
*/
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
/**
* Number of distinct displacement frames to cycle through.
* Higher = smoother wobble, more SVG filters in the DOM.
* @default 5
*/
steps?: number;
/**
* Time between filter swaps, in milliseconds.
* Lower = more frantic; higher = lazier wave.
* @default 80
*/
stepDuration?: number;
/**
* Maximum displacement in px. Bigger = more squiggly.
* Pass a single number for a constant scale, or a tuple to alternate
* between two values per step (matches the original Lucas Bebber demo).
* @default [6, 8]
*/
scale?: number | [number, number];
/**
* Turbulence base frequency. Lower values produce longer, smoother waves;
* higher values produce tighter, jitterier noise.
* @default 0.02
*/
baseFrequency?: number;
/**
* Number of turbulence octaves. Higher = more detailed noise.
* @default 3
*/
numOctaves?: number;
/**
* Render the wrapper as this element type.
* @default "span"
*/
as?: "span" | "div";
}
export function SquigglyText({
children,
steps = 5,
stepDuration = 80,
scale = [6, 8],
baseFrequency = 0.02,
numOctaves = 3,
as = "span",
className,
style,
}: SquigglyTextProps) {
const reactId = useId();
// useId can produce ":" / "_" which aren't valid in CSS url(#…) refs.
const safeId = reactId.replace(/[:_]/g, "");
const filterId = (i: number) => `squiggly-${safeId}-${i}`;
const filters = React.useMemo(
() => Array.from({ length: steps }, (_, i) => `url(#${filterId(i)})`),
// filterId is stable per instance
[steps, safeId]
);
const time = useTime();
const filter = useTransform(time, (t) => filters[Math.floor(t / stepDuration) % filters.length]);
const scaleAt = (i: number) => (Array.isArray(scale) ? scale[i % scale.length] : scale);
const Wrapper = as === "div" ? motion.div : motion.span;
return (
<Wrapper style={{ filter, ...style }} className={cn("inline-block", className)}>
<svg
aria-hidden
className="pointer-events-none absolute h-0 w-0 overflow-hidden"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{Array.from({ length: steps }).map((_, i) => (
<filter id={filterId(i)} key={i}>
<feTurbulence
baseFrequency={baseFrequency}
numOctaves={numOctaves}
result="noise"
seed={i}
/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale={scaleAt(i)} />
</filter>
))}
</defs>
</svg>
{children}
</Wrapper>
);
}
+107
View File
@@ -0,0 +1,107 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { useMotionValueEvent, useScroll } from "motion/react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const StickyScroll = ({
content,
contentClassName,
}: {
content: {
title: string;
description: string;
content?: React.ReactNode | any;
}[];
contentClassName?: string;
}) => {
const [activeCard, setActiveCard] = React.useState(0);
const ref = useRef<any>(null);
const { scrollYProgress } = useScroll({
// uncomment line 22 and comment line 23 if you DONT want the overflow container and want to have it change on the entire page scroll
// target: ref
container: ref,
offset: ["start start", "end start"],
});
const cardLength = content.length;
useMotionValueEvent(scrollYProgress, "change", (latest) => {
const cardsBreakpoints = content.map((_, index) => index / cardLength);
const closestBreakpointIndex = cardsBreakpoints.reduce((acc, breakpoint, index) => {
const distance = Math.abs(latest - breakpoint);
if (distance < Math.abs(latest - cardsBreakpoints[acc])) {
return index;
}
return acc;
}, 0);
setActiveCard(closestBreakpointIndex);
});
const backgroundColors = [
"#0f172a", // slate-900
"#000000", // black
"#171717", // neutral-900
];
const linearGradients = [
"linear-gradient(to bottom right, #06b6d4, #10b981)", // cyan-500 to emerald-500
"linear-gradient(to bottom right, #ec4899, #6366f1)", // pink-500 to indigo-500
"linear-gradient(to bottom right, #f97316, #eab308)", // orange-500 to yellow-500
];
const [backgroundGradient, setBackgroundGradient] = useState(linearGradients[0]);
useEffect(() => {
setBackgroundGradient(linearGradients[activeCard % linearGradients.length]);
}, [activeCard]);
return (
<motion.div
animate={{
backgroundColor: backgroundColors[activeCard % backgroundColors.length],
}}
className="relative flex h-[30rem] justify-center space-x-10 overflow-y-auto rounded-md p-10"
ref={ref}
>
<div className="div relative flex items-start px-4">
<div className="max-w-2xl">
{content.map((item, index) => (
<div key={item.title + index} className="my-20">
<motion.h2
initial={{
opacity: 0,
}}
animate={{
opacity: activeCard === index ? 1 : 0.3,
}}
className="text-2xl font-bold text-slate-100"
>
{item.title}
</motion.h2>
<motion.p
initial={{
opacity: 0,
}}
animate={{
opacity: activeCard === index ? 1 : 0.3,
}}
className="text-kg mt-10 max-w-sm text-slate-300"
>
{item.description}
</motion.p>
</div>
))}
<div className="h-40" />
</div>
</div>
<div
style={{ background: backgroundGradient }}
className={cn(
"sticky top-10 hidden h-60 w-80 overflow-hidden rounded-md bg-white lg:block",
contentClassName
)}
>
{content[activeCard].content ?? null}
</div>
</motion.div>
);
};
+76
View File
@@ -0,0 +1,76 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export const MaskContainer = ({
children,
revealText,
size = 10,
revealSize = 600,
className,
}: {
children?: string | React.ReactNode;
revealText?: string | React.ReactNode;
size?: number;
revealSize?: number;
className?: string;
}) => {
const [isHovered, setIsHovered] = useState(false);
const [mousePosition, setMousePosition] = useState<any>({ x: null, y: null });
const containerRef = useRef<any>(null);
const updateMousePosition = (e: any) => {
const rect = containerRef.current.getBoundingClientRect();
setMousePosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
};
useEffect(() => {
containerRef.current.addEventListener("mousemove", updateMousePosition);
return () => {
if (containerRef.current) {
containerRef.current.removeEventListener("mousemove", updateMousePosition);
}
};
}, []);
const maskSize = isHovered ? revealSize : size;
return (
<motion.div
ref={containerRef}
className={cn("relative h-screen", className)}
animate={{
backgroundColor: isHovered ? "var(--slate-900)" : "var(--white)",
}}
transition={{
backgroundColor: { duration: 0.3 },
}}
>
<motion.div
className="absolute flex h-full w-full items-center justify-center bg-black [mask-image:url(/mask.svg)] [mask-size:40px] [mask-repeat:no-repeat] text-6xl dark:bg-white"
animate={{
maskPosition: `${mousePosition.x - maskSize / 2}px ${mousePosition.y - maskSize / 2}px`,
maskSize: `${maskSize}px`,
}}
transition={{
maskSize: { duration: 0.3, ease: "easeInOut" },
maskPosition: { duration: 0.15, ease: "linear" },
}}
>
<div className="absolute inset-0 z-0 h-full w-full bg-black opacity-50 dark:bg-white" />
<div
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => {
setIsHovered(false);
}}
className="relative z-20 mx-auto max-w-4xl text-center text-4xl font-bold"
>
{children}
</div>
</motion.div>
<div className="flex h-full w-full items-center justify-center">{revealText}</div>
</motion.div>
);
};

Some files were not shown because too many files have changed in this diff Show More