Merge pull request #1386 from PIXELUPLABS/pixelup-dev

GSAP optimisation
This commit is contained in:
Ayush
2026-04-29 11:26:53 +01:00
committed by GitHub
23 changed files with 241 additions and 404 deletions

View File

@@ -332,3 +332,10 @@ body {
color: #b08aff;
text-decoration-color: #b08aff;
}
/* ==========================================================================
HERO REVEAL — desktop only (lg+), driven by GSAP
On mobile, GSAP is skipped and hero elements are immediately visible.
The lg:opacity-0 utility class on .hero-reveal / .hero-cta elements
ensures they start hidden on desktop where GSAP animates them in.
========================================================================== */

View File

@@ -7,11 +7,13 @@ import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
display: "swap",
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
display: "swap",
});
const url = "https://useautumn.com";

View File

@@ -1,25 +1,27 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "@autumn/website",
"dependencies": {
"@gsap/react": "^2.1.2",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.2.4",
"@types/react": "^19.2.14",
"@vercel/analytics": "^2.0.1",
"framer-motion": "^12.38.0",
"gray-matter": "^4.0.3",
"gsap": "^3.15.0",
"lottie-react": "^2.4.1",
"lottie-web": "^5.13.0",
"matter-js": "^0.20.0",
"motion": "^12.38.0",
"next": "16.2.4",
"next-mdx-remote": "^6.0.0",
"ngrok": "^5.0.0-beta.2",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-syntax-highlighter": "^16.1.1",
"remark-frontmatter": "^5.0.0",
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.2",
@@ -159,6 +161,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@mdx-js/loader": ["@mdx-js/loader@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" }, "peerDependencies": { "webpack": ">=5" }, "optionalPeers": ["webpack"] }, "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
@@ -169,6 +173,8 @@
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.1", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA=="],
"@next/mdx": ["@next/mdx@16.2.4", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-e/3bgla+/oF3vDlndI0eFPa0bnP47HPVA0InsAJi7Jr3DwV8WpEGuOcm/3PdI5/93FfNiBhMVeVHZpm1sFlmJw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="],
@@ -805,10 +811,10 @@
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"matter-js": ["matter-js@0.20.0", "", {}, "sha512-iC9fYR7zVT3HppNnsFsp9XOoQdQN2tUyfaKg4CHLH8bN+j6GT4Gw7IH2rP0tflAebrHFw730RR3DkVSZRX8hwA=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
"mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="],
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
@@ -831,6 +837,8 @@
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
"micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="],
"micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
"micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
@@ -907,8 +915,6 @@
"next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": "dist/bin/next" }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="],
"next-mdx-remote": ["next-mdx-remote@6.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^4.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ=="],
"ngrok": ["ngrok@5.0.0-beta.2", "", { "dependencies": { "extract-zip": "^2.0.1", "got": "^11.8.5", "lodash.clonedeep": "^4.5.0", "uuid": "^7.0.0 || ^8.0.0", "yaml": "^2.2.2" }, "optionalDependencies": { "hpagent": "^0.1.2" }, "bin": "bin/ngrok" }, "sha512-UzsyGiJ4yTTQLCQD11k1DQaMwq2/SsztBg2b34zAqcyjS25qjDpogMKPaCKHwe/APRTHeel3iDXcVctk5CNaCQ=="],
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
@@ -1005,6 +1011,8 @@
"rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
"remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="],
"remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
@@ -1137,8 +1145,6 @@
"unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
"unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
@@ -1157,8 +1163,6 @@
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-matter": ["vfile-matter@5.0.1", "", { "dependencies": { "vfile": "^6.0.0", "yaml": "^2.0.0" } }, "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -1217,6 +1221,10 @@
"is-bun-module/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"micromark-extension-frontmatter/fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],

View File

@@ -1,14 +1,22 @@
"use client";
import gsap from "gsap";
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
import type { PixelHoverHandle, PixelIconComponent } from "@/lib/types";
import { getGsap } from "@/lib/lazyGsap";
type GSAPTimeline = {
play: () => GSAPTimeline;
reverse: () => GSAPTimeline;
kill: () => void;
// biome-ignore lint/suspicious/noExplicitAny: structural shim for GSAP's overloaded .to() signature
to: (...args: any[]) => GSAPTimeline;
};
export const DashboardIconPixel = forwardRef<
PixelHoverHandle,
{ Icon: PixelIconComponent }
>(function DashboardIconPixel({ Icon }, ref) {
const iconRef = useRef<SVGSVGElement | null>(null);
const tlRef = useRef<gsap.core.Timeline | null>(null);
const tlRef = useRef<GSAPTimeline | null>(null);
useImperativeHandle(ref, () => ({
restart: () => tlRef.current?.play(),
@@ -16,47 +24,40 @@ export const DashboardIconPixel = forwardRef<
}));
useEffect(() => {
const pixelEls =
iconRef.current?.querySelectorAll<SVGGraphicsElement>(".icon-pixel-path");
if (!pixelEls?.length) return;
const el = iconRef.current;
if (!el) return;
let cancelled = false;
// Sort pixels by visual position: bottom-left → top-right diagonal
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x +
aBox.width / 2 -
(aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
getGsap().then((gsap) => {
if (cancelled) return;
const pixelEls = el.querySelectorAll<SVGGraphicsElement>(".icon-pixel-path");
if (!pixelEls.length) return;
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#FFFFFF",
duration: 0.2,
stagger: 0.01,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.01,
ease: "back.out(3)",
// Sort pixels by visual position: bottom-left → top-right diagonal
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x + aBox.width / 2 - (aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current!
.to(pixels, { opacity: 1, scale: 1.15, fill: "#FFFFFF", duration: 0.2, stagger: 0.01, ease: "power2.out" })
.to(pixels, { scale: 1, duration: 0.01, ease: "back.out(3)" });
});
return () => {
cancelled = true;
tlRef.current?.kill();
};
}, []);

View File

@@ -1,5 +1,5 @@
"use client";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { motion, useMotionValue, useSpring, useTransform } from "motion/react";
import { useEffect, useRef, useState } from "react";
import type { LayoutProps } from "@/lib/types";
import AnimatedFooterImage from "./animated-footer-image";

View File

@@ -1,5 +1,5 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { AnimatedPlusMinus, faqData } from "@/app/constant";
import { cn } from "@/lib/utils";

View File

@@ -1,13 +1,12 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { motion } from "motion/react";
import dynamic from "next/dynamic";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { CTALines, IconCTADocs, IconCTAStart } from "@/app/constant";
import { getGsap } from "@/lib/lazyGsap";
// `AutumnConfig` pulls in `react-syntax-highlighter` + `highlight.js` (~100KB
// gzipped + meaningful parse cost on mobile). It only renders on `xl+`
@@ -88,88 +87,51 @@ export default function Hero() {
};
}, []);
useGSAP(
() => {
gsap.set(".hero-root", { opacity: 0 });
useEffect(() => {
const container = containerRef.current;
if (!container) return;
gsap.set(".hero-bg", {
opacity: 0,
filter: "blur(6px) brightness(1)",
scale: 0.97,
transformOrigin: "center top",
});
let ctx: { revert: () => void } | null = null;
let cancelled = false;
gsap.set(".hero-reveal", {
opacity: 0,
y: 25,
filter: "blur(12px)",
scale: 0.96,
transformOrigin: "center bottom",
});
// Mobile: CSS animations in globals.css handle the hero reveal.
// Guard here so mobile never initiates the ~60KB GSAP dynamic import.
if (window.innerWidth < 1024) return;
gsap.set(".hero-cta", { opacity: 0, scale: 0.95 });
getGsap().then((gsap) => {
if (cancelled) return;
ctx = gsap.context(() => {
gsap.set(".hero-reveal", {
opacity: 0,
y: 25,
scale: 0.96,
transformOrigin: "center bottom",
});
gsap.set(".hero-cta", { opacity: 0, scale: 0.95 });
const tl = gsap.timeline({
defaults: { overwrite: "auto" },
});
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
tl.to(".hero-root", { opacity: 1, duration: 0.3, ease: "none" })
// hero-bg is intentionally NOT hidden — it is the LCP element and
// must be visible from first paint. The brightness flash still runs.
tl.to(".hero-bg", { filter: "brightness(1.6)", duration: 0.125, ease: "power2.in" })
.to(".hero-bg", { filter: "brightness(1)", duration: 0.125, ease: "power2.out" })
.to(".hero-reveal", { opacity: 1, y: 0, scale: 1, duration: 1.1, stagger: 0.1, ease: "power3.out" }, "-=0.2")
.to(".hero-cta", { opacity: 1, scale: 1, duration: 0.3, stagger: 0.06, ease: "back.out(1.5)" }, "-=0.1");
}, container);
});
.to(".hero-bg", {
opacity: 1,
filter: "blur(0px) brightness(1)",
scale: 1,
duration: 0.4,
ease: "power2.out",
})
.to(".hero-bg", {
filter: "blur(0px) brightness(1.6)",
duration: 0.125,
ease: "power2.in",
})
.to(".hero-bg", {
filter: "blur(0px) brightness(1)",
duration: 0.125,
ease: "power2.out",
})
.to(
".hero-reveal",
{
opacity: 1,
y: 0,
filter: "blur(0px)",
scale: 1,
duration: 1.1,
stagger: 0.1,
ease: "power3.out",
},
"-=0.2",
)
.to(
".hero-cta",
{
opacity: 1,
scale: 1,
duration: 0.3,
stagger: 0.06,
ease: "back.out(1.5)",
},
"-=0.1",
);
},
{ scope: containerRef },
);
return () => {
cancelled = true;
ctx?.revert();
};
}, []);
return (
<div ref={containerRef}>
<div className="relative hero-root opacity-0 flex flex-col items-stretch pb-0 md:pb-12 mb-0 bg-[#0F0F0F]">
<div className="relative hero-root flex flex-col items-stretch pb-0 md:pb-12 mb-0 bg-[#0F0F0F]">
<div className="flex justify-between">
<div className="flex flex-col gap-6 px-4 xl:px-22.75 py-8 bg-[#0F0F0F] mt-26">
<h4 className="hero-reveal relative uppercase font-mono tracking-[-2%] text-[12px] md:text-sm leading-sm text-white md:text-[#FFFFFF99] bg-[#2c2c2d] w-fit p-2 min-h-[30px] md:min-h-[36px] flex items-center">
<h4 className="hero-reveal lg:opacity-0 relative uppercase font-mono tracking-[-2%] text-[12px] md:text-sm leading-sm text-white md:text-[#FFFFFF99] bg-[#2c2c2d] w-fit p-2 min-h-[30px] md:min-h-[36px] flex items-center">
<span className="invisible select-none" aria-hidden="true">
{BADGE_TEXT}
</span>
@@ -178,13 +140,13 @@ export default function Hero() {
</span>
</h4>
<div className="flex flex-col gap-6 w-full px-0 lg:px-0">
<h1 className="hero-reveal text-[44px] md:text-[56px] w-full max-w-sm sm:max-w-[480px] md:max-w-xl leading-[44px] tracking-[-5%] md:leading-14 font-sans">
<h1 className="hero-reveal lg:opacity-0 text-[44px] md:text-[56px] w-full max-w-sm sm:max-w-[480px] md:max-w-xl leading-[44px] tracking-[-5%] md:leading-14 font-sans">
<span className="text-[#FFFFFF99] font-normal">
Drop-in credits and billing for
</span>{" "}
<span className="text-white block md:inline">AI agents</span>
</h1>
<p className="hero-reveal tracking-[-2%] w-full max-w-xs sm:max-w-[480px] md:max-w-xl text-[#FFFFFF99] md:text-[16px] text-[14px] font-light leading-5 font-sans">
<p className="hero-reveal lg:opacity-0 tracking-[-2%] w-full max-w-xs sm:max-w-[480px] md:max-w-xl text-[#FFFFFF99] md:text-[16px] text-[14px] font-light leading-5 font-sans">
Stop rebuilding usage limits, credit ledgers and payment logic.{" "}
<span className="text-white font-light">
Autumn is the customer database
@@ -201,7 +163,7 @@ export default function Hero() {
so mobile never downloads them, and desktop fills the
already-reserved space once it hydrates.
*/}
<div className="hero-reveal relative w-[50vw] max-w-[720px] min-h-[525px] p-16 py-0 mx-auto hidden xl:block">
<div className="hero-reveal lg:opacity-0 relative w-[50vw] max-w-[720px] min-h-[525px] p-16 py-0 mx-auto hidden xl:block">
{isXl && (
<>
<div className="absolute inset-0 z-0 pointer-events-none">
@@ -224,7 +186,7 @@ export default function Hero() {
<div className="border-t border-[#292929]" />
<div className="flex flex-nowrap items-center xl:px-22.75 px-4 bg-[#0F0F0F] w-full overflow-hidden">
{/* Primary CTA */}
<div className="hero-cta w-full md:w-fit md:flex-shrink-0">
<div className="hero-cta lg:opacity-0 w-full md:w-fit md:flex-shrink-0">
<Link
href={
isLoggedIn
@@ -253,7 +215,7 @@ export default function Hero() {
</div>
{/* Secondary CTA */}
<div className="hero-cta w-full md:w-fit md:flex-shrink-0">
<div className="hero-cta lg:opacity-0 w-full md:w-fit md:flex-shrink-0">
<Link href={"https://cal.com/ayrod"}>
<motion.div
initial="initial"
@@ -274,7 +236,7 @@ export default function Hero() {
</Link>
</div>
<div className="hero-cta hidden md:flex flex-nowrap gap-2 md:gap-3 ml-2 md:ml-3 h-10.5 md:h-12.5 flex-1">
<div className="hero-cta lg:opacity-0 hidden md:flex flex-nowrap gap-2 md:gap-3 ml-2 md:ml-3 h-10.5 md:h-12.5 flex-1">
<div className="border-r border-[#292929] h-full hidden md:block" />
<div className="border-r border-[#292929] h-full hidden md:block" />
<div className="border-r border-[#292929] h-full hidden md:block" />
@@ -305,11 +267,12 @@ export default function Hero() {
aria-hidden="true"
fill
priority
fetchPriority="high"
sizes="100vw"
className="hero-bg absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 pointer-events-none select-none"
/>
<div className="hero-reveal relative z-10 w-[96%] sm:w-[90%] max-w-[520px] flex justify-center items-center">
<div className="hero-reveal lg:opacity-0 relative z-10 w-[96%] sm:w-[90%] max-w-[520px] flex justify-center items-center">
{/* <AutumnConfig lines={16} initialDelay={1000} awaitEvent="preloader:complete" /> */}
<Image
src={"/images/hero/autumn_mobile.svg"}

View File

@@ -3,11 +3,15 @@
import dynamic from "next/dynamic";
import { useEffect } from "react";
import Hero from "./hero";
import LazySection from "./lazy-section";
import SectionDivider from "./section-divider";
// All below-fold sections are code-split into separate lazy chunks so the
// initial JS bundle only contains the hero. Framer-motion, GSAP ScrollTrigger,
// and lottie-web are pulled into these chunks rather than the main bundle.
// LazySection gates each component behind an IntersectionObserver so chunks
// and their heavy assets (Lottie JSON, ScrollTrigger) only download as the
// user scrolls toward them rather than all at once on page load.
const LogoWall = dynamic(() => import("./logo-wall"));
const Problem = dynamic(() => import("./problem"));
const Solution = dynamic(() => import("./solution"));
@@ -52,22 +56,22 @@ export default function HomeSections() {
</div>
<LogoWall /> */}
<SectionDivider title="THE PROBLEM" />
<Problem />
<LazySection><Problem /></LazySection>
<SectionDivider title="THE SOLUTION" />
<Solution />
<LazySection><Solution /></LazySection>
<SectionDivider title="PRICING MODELS" />
<PricingModels />
<LazySection><PricingModels /></LazySection>
<SectionDivider title="FEATURES" />
<Features />
<LazySection><Features /></LazySection>
<SectionDivider title="TESTIMONIALS" />
<Testimonials />
<LazySection><Testimonials /></LazySection>
<SectionDivider title="PRODUCTION SCALE" />
<ProductionScale />
<LazySection><ProductionScale /></LazySection>
<SectionDivider title="PRICING" />
<Pricing />
<LazySection><Pricing /></LazySection>
<SectionDivider title="FAQ" />
<FAQ />
<Footer />
<LazySection><FAQ /></LazySection>
<LazySection><Footer /></LazySection>
</>
);
}

View File

@@ -0,0 +1,37 @@
"use client";
import { useEffect, useRef, useState, type ReactNode } from "react";
export default function LazySection({ children }: { children: ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const [mounted, setMounted] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
// iOS < 12.2 has no IntersectionObserver — mount everything immediately.
if (!("IntersectionObserver" in window)) {
setMounted(true);
return;
}
const observer = new IntersectionObserver(
([entry]) => {
// intersectionRatio > 0 guards against a Safari bug where
// isIntersecting fires as false on the initial sync callback.
if (entry.isIntersecting || entry.intersectionRatio > 0) {
setMounted(true);
observer.disconnect();
}
},
{ rootMargin: "1000px 0px" },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
// min-height: 1px prevents the wrapper collapsing to zero height before
// content mounts, which would cause iOS to jump scroll position on mount.
return <div ref={ref} style={{ minHeight: "1px" }}>{mounted ? children : null}</div>;
}

View File

@@ -1,7 +1,5 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
@@ -29,6 +27,7 @@ import type {
PixelIconComponent,
} from "@/lib/types";
import { cn } from "@/lib/utils";
import { getGsap } from "@/lib/lazyGsap";
import { DashboardIconPixel } from "./dashboard-icon-pixel";
const NAV_LINKS = [
@@ -42,10 +41,18 @@ const NAV_LINKS = [
},
];
type GSAPTimeline = {
play: () => GSAPTimeline;
reverse: () => GSAPTimeline;
kill: () => void;
// biome-ignore lint/suspicious/noExplicitAny: structural shim for GSAP's overloaded .to() signature
to: (...args: any[]) => GSAPTimeline;
};
const NavIconPixel = forwardRef<PixelHoverHandle, { Icon: PixelIconComponent }>(
function NavIconPixel({ Icon }, ref) {
const iconRef = useRef<SVGSVGElement | null>(null);
const tlRef = useRef<gsap.core.Timeline | null>(null);
const tlRef = useRef<GSAPTimeline | null>(null);
useImperativeHandle(ref, () => ({
restart: () => tlRef.current?.play(),
@@ -53,46 +60,39 @@ const NavIconPixel = forwardRef<PixelHoverHandle, { Icon: PixelIconComponent }>(
}));
useEffect(() => {
const pixelEls =
iconRef.current?.querySelectorAll<SVGPathElement>(".icon-pixel-path");
if (!pixelEls?.length) return;
const el = iconRef.current;
if (!el) return;
let cancelled = false;
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x +
aBox.width / 2 -
(aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
getGsap().then((gsap) => {
if (cancelled) return;
const pixelEls = el.querySelectorAll<SVGPathElement>(".icon-pixel-path");
if (!pixelEls.length) return;
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#FFFFFF",
duration: 0.01,
stagger: 0.025,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.01,
ease: "back.out(3)",
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x + aBox.width / 2 - (aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current!
.to(pixels, { opacity: 1, scale: 1.15, fill: "#FFFFFF", duration: 0.01, stagger: 0.025, ease: "power2.out" })
.to(pixels, { scale: 1, duration: 0.01, ease: "back.out(3)" });
});
return () => {
cancelled = true;
tlRef.current?.kill();
};
}, []);
@@ -189,137 +189,21 @@ export default function Navbar({
};
}, [menuOpen]);
useGSAP(
() => {
if (!animateIntro) return;
// Skip the entrance animation on non-desktop viewports, or when
// hydration landed well after first paint. On mobile the GSAP chunk
// can arrive many seconds after the server-rendered navbar has
// already painted; hiding it with `gsap.set({ opacity: 0 })` at
// that point and re-animating it in is a visible flash that's much
// worse than no animation.
if (
window.matchMedia("(max-width: 1023px)").matches ||
performance.now() > 300
) {
return;
}
gsap.set(".nav-root", { opacity: 0 });
gsap.set(".nav-logo", {
opacity: 0,
filter: "blur(6px) brightness(1)",
scale: 0.92,
transformOrigin: "left center",
});
gsap.set(".nav-link", { opacity: 0, y: -8 });
gsap.set(".nav-dashboard", { opacity: 0, scale: 0.95 });
const navMobileRef = useRef<HTMLDivElement | null>(null);
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
tl.to(".nav-root", { opacity: 1, duration: 0.3, ease: "none" })
.to(".nav-logo", {
opacity: 1,
filter: "blur(0px) brightness(1)",
scale: 1,
duration: 0.7,
ease: "power2.out",
})
.to(".nav-logo", {
filter: "blur(0px) brightness(1.6)",
duration: 0.225,
ease: "power2.in",
})
.to(".nav-logo", {
filter: "blur(0px) brightness(1)",
duration: 0.125,
ease: "power2.out",
})
.to(
".nav-link",
{
opacity: 1,
y: 0,
duration: 0.25,
stagger: 0.06,
ease: "power2.out",
},
"-=0.05",
)
.to(
".nav-dashboard",
{
opacity: 1,
scale: 1,
duration: 0.3,
ease: "back.out(1.5)",
},
"-=0.1",
);
},
{ scope: containerRef, dependencies: [animateIntro] },
);
const mobileTl = useRef<gsap.core.Timeline | null>(null);
useGSAP(
() => {
gsap.set(".nav-mobile", {
opacity: 0,
clipPath: "inset(0% 0 100% 0)",
pointerEvents: "none",
});
gsap.set(
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
{
opacity: 0,
filter: "blur(8px)",
scale: 0.95,
},
);
mobileTl.current = gsap.timeline({
paused: true,
defaults: { overwrite: "auto" },
});
mobileTl.current
.to(".nav-mobile", {
opacity: 1,
y: 0,
pointerEvents: "auto",
clipPath: "inset(0% 0 0% 0)",
duration: 0.2,
ease: "power3.inOut",
})
.to(
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
{
opacity: 1,
filter: "blur(0px)",
scale: 1,
duration: 0.15,
stagger: 0.01,
ease: "power2.out",
},
"-=0.1",
);
},
{ scope: containerRef },
);
useGSAP(
() => {
if (mobileTl.current) {
if (menuOpen) {
mobileTl.current.timeScale(1).play();
} else {
mobileTl.current.timeScale(1.8).reverse();
}
}
},
{ scope: containerRef, dependencies: [menuOpen] },
);
useEffect(() => {
const el = navMobileRef.current;
if (!el) return;
if (menuOpen) {
el.style.opacity = "1";
el.style.pointerEvents = "auto";
el.style.clipPath = "inset(0% 0 0% 0)";
} else {
el.style.opacity = "0";
el.style.pointerEvents = "none";
el.style.clipPath = "inset(0% 0 100% 0)";
}
}, [menuOpen]);
return (
<div
@@ -404,8 +288,9 @@ export default function Navbar({
</nav>
</div>
<div
ref={navMobileRef}
className={cn(
"fixed nav-mobile inset-x-0 z-40 flex h-[calc(100dvh-58px)] flex-col overflow-x-hidden overflow-y-auto bg-[#000000] px-4 pb-8 font-mono uppercase transition-all duration-300 md:px-(--page-pad) lg:top-5",
"fixed inset-x-0 z-40 flex h-[calc(100dvh-58px)] flex-col overflow-x-hidden overflow-y-auto bg-[#000000] px-4 pb-8 font-mono uppercase md:px-(--page-pad) lg:top-5",
scrolled && !recoilHidden
? "top-[58px] sm:top-[60px]"
: "top-[66px] sm:top-[62px]",
@@ -414,6 +299,7 @@ export default function Navbar({
opacity: 0,
pointerEvents: "none",
clipPath: "inset(0% 0 100% 0)",
transition: "opacity 0.2s ease, clip-path 0.2s cubic-bezier(0.87, 0, 0.13, 1)",
}}
>
{/* Nav items */}

View File

@@ -66,9 +66,16 @@ export default function SolutionAnimation() {
// Defer the 1.42.1 MB Lottie JSON fetch until the element is close to
// the viewport. Without this the browser fetches it during initial load
// and blocks the main thread while parsing the large JSON blob.
if (!("IntersectionObserver" in window)) {
initAnimation();
return;
}
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
// intersectionRatio > 0 guards against a Safari bug where
// isIntersecting fires as false on the initial sync callback.
if (entries[0].isIntersecting || entries[0].intersectionRatio > 0) {
observer.disconnect();
initAnimation();
}

View File

@@ -0,0 +1,10 @@
// Loads GSAP exactly once — first call triggers the dynamic import,
// subsequent calls reuse the same promise.
type Gsap = (typeof import("gsap"))["default"];
let promise: Promise<Gsap> | null = null;
export const getGsap = (): Promise<Gsap> => {
if (!promise) promise = import("gsap").then((m) => m.default);
return promise;
};

View File

@@ -7,13 +7,13 @@ const nextConfig = {
pageExtensions: ["ts", "tsx", "md", "mdx"],
allowedDevOrigins: ["*.ngrok-free.dev"],
experimental: {
optimizePackageImports: ["framer-motion", "motion", "gsap", "@gsap/react"],
optimizePackageImports: ["motion", "gsap", "@gsap/react"],
optimizeCss: true,
},
images: {
// Serve AVIF to supporting browsers (better compression than WebP),
// falling back to WebP. Next.js negotiates via Accept header automatically.
// formats: ["image/avif", "image/webp"],
formats: ["image/avif", "image/webp"],
},
async headers() {
if (!isProd) return [];

View File

@@ -16,12 +16,9 @@
"@next/mdx": "^16.2.4",
"@types/react": "^19.2.14",
"@vercel/analytics": "^2.0.1",
"framer-motion": "^12.38.0",
"gray-matter": "^4.0.3",
"gsap": "^3.15.0",
"lottie-react": "^2.4.1",
"lottie-web": "^5.13.0",
"matter-js": "^0.20.0",
"motion": "^12.38.0",
"next": "16.2.4",
"ngrok": "^5.0.0-beta.2",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 98 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.7 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 996 KiB