Pular para o conteúdo
Zumkai

Motions premium em Next.js: os 18 padrões do site da Plantica, passo a passo

O scroll vira CSS custom property e o CSS faz o resto: os 18 padrões de motion do site da Plantica em Next.js, com o prompt de implementação pronto de cada um.

  • motion
  • gsap
Home do site da Plantica: hero tipográfico “Plantica is a flower creative studio” com a navegação em pílulas no topo
Sumário
  1. A filosofia que muda tudo: CSS-first, JS-orquestrado
  2. A stack e o papel de cada peça
  3. O mapa dos 18 motions
  4. Passo 1 — A fundação: infraestrutura de animação
  5. Passo 2 — O motor: scroll vira CSS custom property (Motion 4)
  6. Passo 3 — A assinatura tipográfica: texto clip/split (Motion 3)
  7. Passo 4 — Estados globais: primeira carga e transições de página
  8. Passo 5 — O chrome: menu card 3D, botões e underlines
  9. Passo 6 — O cartão de visitas: hero collage com zoom progressivo (Motion 5)
  10. Passo 7 — Seções em série: arquivo, painéis sticky e vídeo-íris
  11. Passo 8 — Polish de desktop: cursor contextual e preview flutuante
  12. Passo 9 — A página de detalhe: split-scroll, zoom FLIP e relacionados
  13. Passo 10 — Scroll infinito nas galerias (Motion 6)
  14. Passo 11 — O fechamento: sprites decorativos e o rodapé em três atos
  15. As 15 armadilhas que vão te morder (leia antes de codar)
  16. Por onde começar hoje

Todo desenvolvedor já viu um daqueles sites de estúdio criativo que parecem de outro campeonato: o texto entra letra por letra, o hero cresce até engolir a tela, o rodapé é revelado como cortina, o cursor conta o que cada área faz. E a pergunta que fica é sempre a mesma: como isso é feito de verdade — e como eu reproduzo num projeto Next.js sem virar um Frankenstein de bibliotecas?

Este guia responde com engenharia reversa de um caso concreto: o site da Plantica (plantica.net), o estúdio criativo floral japonês fundado por Takashi Kimura. "Plantica is a flower creative studio", anuncia o hero — e por trás da aparência delicada há um build customizado exemplar: GSAP + ScrollTrigger + Lenis + barba.js + WebGL sobre um CMS headless. Os 18 padrões abaixo foram catalogados auditando esse site, mas o material é agnóstico de conteúdo: nomes, textos e imagens são placeholders parametrizáveis. O que importa — e o que você leva daqui — é o padrão de movimento, a técnica por trás e um prompt de implementação pronto para colar no seu agente de IA ou usar como especificação.

A ordem dos passos não é a ordem do catálogo: é a ordem de construção. Cada passo assenta a base do seguinte, então dá para seguir o guia como um roteiro de projeto, do zero ao site completo.

A filosofia que muda tudo: CSS-first, JS-orquestrado

Antes de qualquer linha de código, entenda a decisão de arquitetura que sustenta o site auditado — porque ela é o oposto do que a maioria faz.

A abordagem comum é tweenar tudo via JavaScript: cada elemento animado vira um tween GSAP ou um componente Framer Motion, e o JS recalcula valores a cada frame. Funciona, mas escala mal: dezenas de tweens rodando no main thread, cleanup frágil, e qualquer refactor de layout quebra timings.

O site auditado faz diferente. O JavaScript quase não anima nada diretamente. Ele tem só dois empregos:

  • Escrever o progresso do scroll em CSS custom properties (--p1 e --p2), via um único padrão de ScrollTrigger com scrub.
  • Alternar classes de estado no <html>: is-loaded, is-leaving, is-menu-opened, data-shown.

Quem anima de verdade são transitions e calc() no CSS, rodando no compositor. O resultado: performance de outra categoria (nada de layout thrashing por frame), manutenção trivial (os motions viram declarações no CSS e atributos no markup) e um mapa mental simples — JS mede e orquestra, CSS move.

A stack e o papel de cada peça

BibliotecaPapel no projetoPor quê
GSAP 3.13+ e @gsap/reactMotor de progresso de scroll (ScrollTrigger scrub → CSS vars) e orquestrações pontuaisGSAP é 100% gratuito, incluindo todos os plugins, desde a aquisição pela Webflow. Aqui ele é usado de forma minimalista: uma factory de ScrollTrigger para o site inteiro
ScrollTrigger (plugin GSAP)Escrever --p1 e --p2 nos elementos com data-start e data-endscrub curtíssimo (0.01–0.1) para progresso quase cru; o smoothing visual vem do Lenis + CSS
LenisSmooth scroll global e modo infinito em páginas de galeriaO site auditado usa Lenis inclusive com infinite: true para loops verticais de imagens
Framer Motion (motion)Transições de rota (template.tsx) e estados de UI pontuaisSubstitui o papel do barba.js do site original dentro do paradigma App Router
OGL (ou WebGL puro)Canvas de gradiente com blur que segue o mouseO original usa WebGL puro de poucos KB; OGL dá o mesmo resultado com menos boilerplate que three.js
CSS custom properties e transitionsO verdadeiro motor de animaçãotransform: calc(...) consumindo --p1/--p2; transitions com delays por data-d; keyframes para marquee

Não é necessário nenhum plugin de carrossel: as listas horizontais usam overflow + drag nativo. Se preferir abstração, Embla Carousel é compatível com todos os padrões daqui.

O mapa dos 18 motions

#MotionUso típicoGatilho
1Splash/preloader + entrada orquestradaPrimeira carga do siteLoad
2Transição de página (saída/entrada)Toda navegação internaNavegação
3Sistema de texto clip/splitTítulos e parágrafos, o site inteiroLoad/Scroll
4Motor scroll → CSS vars (--p1/--p2)Base dos motions 5, 12, 13, 14, 17, 18Scroll
5Hero collage com zoom progressivoHero da homeScroll
6Smooth scroll + scroll infinitoGlobal + galerias de detalheScroll
7Menu card 3DHeader (botão Menu)Clique
8Botões ghost + underlines animadosBotões e links globaisHover
9Cursor customizado com labelsDesktop, áreas de mídiaMouse
10Preview flutuante em hover de listaListas de itensHover
11Arquivo: reveals, sort sticky, filtrosPáginas de listagemScroll/Clique
12Painéis sobre background stickySeção de destaque na homeScroll/Hover
13Vídeo com expansão por clip-pathSeção de vídeo institucionalScroll
14Página de detalhe split-scrollDetalhe de projeto/caseScroll
15Click-to-zoom de imagem (FLIP)Galerias de detalheClique
16Lista horizontal com dragRelacionados/próximos itensArraste
17Sprites decorativos (pop + parallax)Página institucionalLoad/Scroll
18Rodapé compostoRodapé globalScroll/Mouse

Reveals genéricos de seção não têm número próprio: eles reutilizam o sistema de texto do Motion 3 e o padrão de fade/clip com data-shown via IntersectionObserver.

Passo 1 — A fundação: infraestrutura de animação

Tudo depende deste passo. Aqui nasce o provider de smooth scroll, o registro único dos plugins GSAP, os tokens de easing no CSS e os dois hooks que o site inteiro consome. Repare em três decisões que costumam ser ignoradas em tutoriais e que aqui são inegociáveis: o cleanup do Lenis em três passos (sem ele, cada navegação vaza um ticker), os easings morando no CSS (porque é o CSS que anima) e o hook de prefers-reduced-motion desde o dia zero — acessibilidade não é camada final, é fundação.

txt
Em um projeto Next.js 14+ com App Router, TypeScript e Tailwind, configure a
infraestrutura de animação:

1. Instale: gsap @gsap/react lenis motion ogl
2. Crie um componente client "SmoothScrollProvider" que:
   - Instancia Lenis com { lerp: 0.1, wheelMultiplier: 1, smoothWheel: true }
   - Sincroniza com ScrollTrigger usando uma função nomeada (necessária
     para o cleanup):
       const update = (time: number) => lenis.raf(time * 1000);
       lenis.on('scroll', ScrollTrigger.update);
       gsap.ticker.add(update);
       gsap.ticker.lagSmoothing(0);
   - Cleanup COMPLETO no unmount (nesta ordem):
       gsap.ticker.remove(update);   // sem isso o ticker segue chamando
       lenis.off('scroll', ScrollTrigger.update);   // raf() de instância
       lenis.destroy();                             // morta (leak)
   - Exponha a instância via React Context com um hook useLenis(), para
     outros componentes chamarem lenis.stop()/start() (menu, zoom).
3. Registre os plugins uma única vez num arquivo lib/gsap.ts:
   gsap.registerPlugin(ScrollTrigger, useGSAP) e exporte gsap.
   Esse arquivo só deve ser importado por componentes client.
4. Crie o MOTOR CENTRAL do site: hook "useScrollProgress" (detalhado no
   Motion 4) que lê elementos [data-scroll-trigger] e escreve --p1/--p2.
5. Crie um hook usePrefersReducedMotion() lendo a media query
   (prefers-reduced-motion: reduce); todos os componentes de animação
   devem pular ou reduzir animações quando true.
6. Defina no globals.css os tokens de easing e duração como CSS vars
   (o site inteiro anima via CSS, então os easings moram no CSS):
   :root {
     /* aproximações CSS dos easings GSAP (valores de easings.net) */
     --ease-power2-out: cubic-bezier(0.5, 1, 0.89, 1);      /* quadOut  */
     --ease-power4-out: cubic-bezier(0.25, 1, 0.5, 1);      /* quartOut */
     --ease-power2-in-out: cubic-bezier(0.45, 0, 0.55, 1);  /* quadInOut */
     --a-clip-s: 1s;        /* duração dos reveals de texto */
     --a-global-t-s: 0.8s;  /* duração de UI global (menu, header) */
     --a-hover-s: 0.4s;     /* duração de hovers */
     --vh: 1vh;             /* corrigido via JS p/ mobile (resize) */
   }
7. Envolva {children} no layout.tsx raiz com <SmoothScrollProvider>.
Entregue os arquivos completos e o layout.tsx atualizado.

Passo 2 — O motor: scroll vira CSS custom property (Motion 4)

Este é o coração de tudo, e é invisível por definição: é a fiação. Um único mecanismo alimenta o hero que cresce, os painéis que deslizam, o vídeo que expande e o rodapé que se revela. Implemente-o antes de qualquer motion visual — os motions 5, 12, 13, 14, 17 e 18 são só consumidores dele.

O funcionamento: uma factory de ScrollTrigger varre os elementos marcados com data-scroll-trigger, lê os atributos data-start e data-end e, com scrub curtíssimo, escreve em cada elemento duas variáveis — --p1 (progresso, 0 a 1) e --p2 (o inverso). Todo o resto é CSS: transform: translateY(calc(var(--p1) * -50vh)), scale(calc(1 + var(--p2) * 0.5)). Compositor-friendly, zero re-render de React, e os motions viram declarativos no markup.

O detalhe que separa o funcional do profissional: inicializar --p1: 0 e --p2: 1 no CSS, não só via JS. É isso que faz o HTML do servidor já renderizar o estado de topo, sem flash de layout no primeiro paint — essencial num projeto Next.js com SSR.

txt
Crie o motor de progresso de scroll em Next.js:

1. Hook client "useScrollProgress(scopeRef)" que, dentro de useGSAP:
   gsap.utils.toArray('[data-scroll-trigger]', scope).forEach((el) => {
     ScrollTrigger.create({
       trigger: el,
       start: el.dataset.start ?? 'top bottom',
       end: el.dataset.end ?? 'bottom top',
       scrub: 0.01,
       onUpdate: (self) => {
         el.style.setProperty('--p1', String(self.progress));
         el.style.setProperty('--p2', String(1 - self.progress));
       }
     });
   });
   Inicialize --p1: 0 e --p2: 1 via CSS (:where([data-scroll-trigger])
   { --p1: 0; --p2: 1 }) para o SSR renderizar o estado de topo sem
   flash de layout.
2. Sintaxe declarativa no markup (idêntica ao site auditado):
   <section data-scroll-trigger data-start="top top"
            data-end="bottom+=20% top">...</section>
3. Os elementos filhos consomem as vars SÓ com transform/opacity
   (nunca width/height/top: causam layout). Exemplos:
   .bg   { transform: scale(calc(1 + var(--p2) * 0.5)); }
   .card { transform: translateY(calc(var(--p1) * -100vh)); }
   .box  { border-radius: calc(var(--r) * var(--p2)); }
4. will-change: transform apenas nos elementos que de fato animam, e
   de preferência togglado (via classe) quando a seção está no viewport.
5. Chame ScrollTrigger.refresh() após load de imagens/fontes e após
   transições de rota (Motion 2).
6. prefers-reduced-motion: não crie os triggers; congele --p1: 0/--p2: 1
   (estado inicial estático).

Passo 3 — A assinatura tipográfica: texto clip/split (Motion 3)

Nenhum texto do site simplesmente aparece. Títulos entram letra por letra, cada caractere subindo de trás de uma linha de corte invisível, com atraso mínimo entre eles — o efeito onda. Textos menores entram por palavra ou por linha. E o mesmo mecanismo, ao contrário, faz os textos saírem nas transições de página. É a assinatura "editorial de luxo" do site inteiro, e é usada por praticamente todos os outros motions — por isso vem antes deles.

A mecânica: cada unidade de texto é envolvida num <span class="o"> (máscara com overflow: hidden) contendo um <span class="t"> (o texto, começando em translateY(110%)). A animação é uma transition CSS disparada pela mudança do atributo data-shown, que um IntersectionObserver (ou o load do Passo 4) alterna. Os delays em cascata vêm do índice do caractere, via CSS var.

txt
Crie o sistema de reveal de texto em Next.js:

1. Componente client "SplitReveal" com props:
   type: 'chars' | 'words' | 'lines', delay (s entre unidades,
   default 0.05), trigger: 'load' | 'inview', as: tag do elemento.
2. Split: use SplitText do GSAP (gratuito no 3.13+) com
   { type, mask: type } dentro de useGSAP, apenas para GERAR os
   wrappers (.o overflow-hidden > .t); a animação em si é CSS.
   Aguarde as fontes com autoSplit: true (ou document.fonts.ready)
   para não quebrar linhas erradas.
3. CSS:
   .split .t {
     display: inline-block;
     transform: translate3d(0, 110%, 0);
     transition: transform var(--a-clip-s) var(--ease-power4-out),
                 opacity 0.6s;
     transition-delay: var(--d, 0s);
   }
   [data-shown="true"] .t { transform: translate3d(0, 0, 0); }
   Atribua --d por unidade: style={{ '--d': `${i * delay}s` }}.
4. trigger 'inview': hook useInView (IntersectionObserver,
   threshold 0.2, once) seta data-shown="true".
   trigger 'load': reage à classe is-loaded-a do Motion 1.
5. Compatibilidade com o Motion 2: quando <html> tem .is-leaving, o CSS
   global sobrescreve com saída (translateY(-50%), fade, delay 0).
6. Cuidados: aria-label com o texto original no elemento pai e
   aria-hidden nos spans (leitores de tela não devem soletrar);
   prefers-reduced-motion troca tudo por fade simples.
7. Kerning fino: exponha uma prop para ajustes por caractere
   (letter-spacing negativo em pares específicos), aplicada via
   data-char, como faz o site auditado.

Passo 4 — Estados globais: primeira carga e transições de página

Estes dois motions estabelecem as classes de estado (is-loaded, is-leaving) que o CSS inteiro referencia. Sem eles, os reveals do Passo 3 não têm quem os dispare no load, e a navegação vira corte seco.

Motion 1 — Splash e entrada orquestrada

A página não estoura na tela de uma vez. Primeiro tudo fica invisível por um instante; então os elementos do topo entram em coreografia: as letras do logo sobem uma a uma de dentro de uma máscara, os itens do menu aparecem em sequência, e só então o conteúdo principal começa o próprio reveal. Cortina de teatro abrindo, tudo no seu tempo.

E aqui está a elegância da arquitetura: nenhum tween JS. O <html> começa com uma classe de "não carregado"; quando fontes e assets críticos carregam, o JS adiciona is-loaded (e, um frame depois, is-loaded-a). Todo o resto são transitions CSS com transition-delay por elemento — um atributo data-d="1..N" que vira transition-delay: calc(var(--d) * 0.05s).

txt
Implemente a entrada orquestrada em Next.js:

1. No globals.css: body { visibility: hidden } e
   .is-loaded body { visibility: visible } (anti-FOUC bruto, igual ao
   padrão de sites premium). Inclua um <noscript> com CSS alternativo
   que força visibility: visible (acessibilidade sem JS).
2. Componente client "LoadOrchestrator" montado no layout raiz:
   - Aguarda document.fonts.ready e o load dos assets críticos do hero
     (Promise.all com decode() das imagens above-the-fold).
   - Adiciona 'is-loaded' no <html>; num requestAnimationFrame seguinte,
     adiciona 'is-loaded-a' (dois estágios permitem que o CSS distinga
     "pronto" de "pode animar").
3. Elementos do header (letras do logo, itens de nav) usam o padrão de
   máscara do Motion 3, cada um com atributo data-d="1..N". No CSS:
   [data-d] .t { transition-delay: calc(var(--d) * 0.06s) }
   (passe o número para a CSS var --d via style inline; não use attr()
   em calc, o suporte dos browsers ainda é limitado).
   Estado inicial: translateY(110%); com .is-loaded-a: translateY(0).
4. Opcional: logotipo do splash central com fade 1.2s antes do header.
5. prefers-reduced-motion: pule os delays e revele tudo com fade curto.
6. Garanta que a orquestração rode UMA vez por sessão (sessionStorage);
   em navegações internas, quem manda é o Motion 2.

Motion 2 — Transição de página

Ao clicar num link, a página atual não some num piscar: os textos escorregam para cima e desmancham em fade (~0.6s), e a página nova entra com os títulos subindo das máscaras, na mesma linguagem do primeiro load. A navegação vira continuação da coreografia.

No site original esse papel era do barba.js. Em Next.js, o App Router assume: Link e prefetch nativos cuidam da troca; a saída vem de uma classe global is-leaving aplicada antes do router.push (com um pequeno await), e a entrada vem do template.tsx remontando e o sistema de reveal reagindo ao novo mount.

txt
Implemente transições de página em Next.js App Router:

1. Crie um TransitionProvider (client) com contexto { isLeaving,
   navigate(href) }:
   - navigate: seta isLeaving=true (adiciona classe 'is-leaving' no
     <html>), aguarda ~600ms (duração da saída) e chama router.push(href).
   - Todos os links internos usam um componente <TransitionLink> que
     intercepta o clique e chama navigate (mantendo <Link> por baixo
     para preservar prefetch).
2. CSS de saída (mesma linguagem do site auditado):
   .is-leaving .c-clip .t, .is-leaving .c-fade {
     transition: all 0.6s var(--ease-power2-in-out) !important;
     transition-delay: 0s !important;   /* ignora delays de entrada */
     opacity: 0 !important;
     transform: translateY(-50%) !important;
   }
3. Entrada: template.tsx remonta a cada navegação; nele, um efeito
   remove 'is-leaving' e re-dispara o pipeline do Motion 3 (os títulos
   da nova página entram pelas máscaras). Um fade global do container
   (Framer Motion, opacity 0→1, 0.35s) cobre o instante da troca.
4. Ao trocar de rota: lenis.scrollTo(0, { immediate: true }) antes da
   entrada, e ScrollTrigger.refresh() depois que as imagens da nova
   página carregarem.
5. Acessibilidade: anuncie a navegação (aria-live) e respeite
   prefers-reduced-motion com corte simples de fade.
6. Não bloqueie navegações do browser (voltar/avançar): detecte popstate
   e pule a fase de saída nesses casos.

Passo 5 — O chrome: menu card 3D, botões e underlines

O menu valida os estados globais que você acabou de criar, e os botões são a vitória rápida que aparece em todo lugar do site.

Motion 7 — Menu card 3D com flip de label

Ao clicar em "Menu", um cartão arredondado se materializa no canto superior: ele assenta com sensação de profundidade (escala + deslocamento sob perspectiva 3D, como uma carta pousando na mesa), enquanto o rótulo do botão vira de "Menu" para "Close" com um flip vertical. Dentro, os links entram em cascata, com contadores discretos ao lado.

O truque de arquitetura: sem AnimatePresence. O cartão existe sempre no DOM (ótimo para o SEO dos links) e alterna estado por classe is-menu-opened no <html>. O pai tem perspective: 200vw; o cartão sai de scale(0.95) translateY(1rem) + opacity 0 para identidade, tudo transition CSS.

txt
Crie um componente client "MenuCard" em Next.js:

1. Estrutura fixa no canto: div.menu (pointer-events none fechado) >
   div.in (perspective: 200vw) > [header com botão toggle] +
   div.menu-bg (fundo arredondado) + div.menu-body (conteúdo).
2. Estados via classe no <html> (useEffect togglando 'is-menu-opened'):
   .menu-bg, .menu-body {
     transform: scale(.95) translateY(1rem);
     transform-origin: top center; opacity: 0;
     transition: opacity .3s,
                 transform var(--a-global-t-s) var(--ease-power4-out);
   }
   .is-menu-opened .menu-bg, .is-menu-opened .menu-body {
     transform: scale(1) translate(0); opacity: 1;
   }
   O site auditado define perspective mas mantém rotação em 0 (a
   profundidade vem do scale + translate sob perspective). Se quiser
   acentuar o 3D, adicione rotateX(-4deg) ao estado fechado (opcional).
3. Botão toggle com flip de label: máscara overflow-hidden com DUAS
   divs empilhadas ("Menu" / "Close"); aberto → translateY(-100%)
   revela "Close". O mesmo botão dentro do cartão fecha (aria-expanded,
   aria-controls corretos).
4. Links principais com o split do Motion 3 (delays em cascata) e
   contador discreto ao lado (número de itens, vindo do CMS);
   links secundários/sociais em grade; seletor de idioma em bolinhas
   circulares (aria-current no ativo).
5. Comportamento: Escape fecha; clique fora fecha; lenis.stop()/start()
   ao abrir/fechar; foco preso no cartão enquanto aberto e devolvido
   ao botão ao fechar; data-lenis-prevent se o cartão rolar.
6. Ao navegar por um link do menu: feche o cartão em paralelo à
   transição do Motion 2.
7. prefers-reduced-motion: fade simples sem rotate/scale.

Motion 8 — Botões ghost e underlines animados

Duas famílias de micro-interação, 100% CSS. Nos botões-pílula, o rótulo escorrega para cima e é substituído por uma cópia idêntica vinda de baixo (flip com máscara), enquanto um círculo de cor infla de dentro do botão até preencher a pílula — o fundo nasce redondo e se espalha. Nos links de texto, um sublinhado desliza para fora e volta pelo outro lado, ou se desenha da esquerda para a direita.

O micro-detalhe que dá o acabamento: no flip, a primeira cópia viaja para -120% e a segunda para -100% — esse descompasso mínimo entre as duas é o micro-stagger que faz o movimento parecer orgânico em vez de mecânico.

txt
Crie os componentes "GhostButton" e "UnderlineLink" em Next.js:

1. GhostButton (pílula com borda):
   - Markup: <a class="btn"> <span class="o"> <span class="w">Label
     </span><span class="w" aria-hidden>Label</span> </span> </a>
   - CSS:
     .btn { position: relative; overflow: hidden; border-radius: 2em;
            border: 1px solid var(--col-border); }
     .btn::after { content: ''; position: absolute; inset: 0;
       background: var(--col-black); border-radius: 50%;
       transform: translateY(100%); z-index: 0;
       transition: border-radius .3s var(--ease-power4-out),
                   transform var(--a-global-t-s) var(--ease-power4-out); }
     .btn .o { overflow: hidden; position: relative; z-index: 1; }
     .btn .w { display: block; transition: transform
               var(--a-global-t-s) var(--ease-power4-out),
               color .3s; }
     .btn .w:last-child { position: absolute; top: 100%; }
     .btn:hover::after { border-radius: 2em; transform: translate(0); }
     .btn:hover .w { transform: translateY(-100%); color: var(--col-white); }
     .btn:hover .w:first-child { transform: translateY(-120%); }
     (o -120% vs -100% cria o micro-stagger entre as cópias)
   - Estado "current"/ativo reaproveita o hover de forma persistente.
2. UnderlineLink:
   - Variante slide: <span class="b"> absoluta na base, translateX(-101%),
     hover → translateX(0), transition var(--a-hover-s)
     var(--ease-power2-out).
   - Variante draw (links de texto corrido): background-image
     linear-gradient, background-size 0 1px → 100% 1px via keyframes
     no hover (permite "redesenhar" a cada hover).
3. Rotas internas: use next/link (ou o TransitionLink do Motion 2);
   externos: <a> com rel="noopener".
4. Hover states só em dispositivos com hover real:
   @media (hover: hover); em touch, os estados ficam estáticos.
5. focus-visible:ring sempre presente; nunca remova outline sem repor.

Passo 6 — O cartão de visitas: hero collage com zoom progressivo (Motion 5)

O topo do site é uma colagem: uma imagem central e várias menores espalhadas ao redor, todas com cantos arredondados, entrando em cascata no load. Ao rolar, a mágica: a imagem central cresce continuamente até tomar a tela inteira, os cantos vão relaxando até o retângulo puro, e as satélites dão zoom interno e saem de cena. A rolagem parece entrar dentro da imagem.

Este é o primeiro grande consumidor do motor do Passo 2 — e o teste de que sua fundação está certa. A estrutura: container de 200–230vh de altura com um filho position: sticky; top: 0; height: 100vh. O container é um data-scroll-trigger de top top a bottom top; tudo dentro anima por CSS consumindo --p1 e --p2.

txt
Crie um componente client "HeroCollage" em Next.js:

1. Estrutura:
   <section class="hero" data-scroll-trigger data-start="top top"
            data-end="bottom+=20% top">   <!-- height: 230vh -->
     <div class="hero-sticky">            <!-- sticky top-0 h-screen,
                                               overflow-hidden -->
       <div class="hero-mv">…visual central…</div>
       <div class="hero-img" data-x="0" data-y="0">…</div>
       <!-- N imagens satélites posicionadas por data-x/data-y -->
     </div>
   </section>
2. CSS (consumindo o motor do Motion 4):
   .hero-mv {
     transform: scale(calc(0.6 + var(--p1) * 0.4 + var(--p2) * 0.1))
                translate(calc(var(--hero-x) * var(--p2)),
                          calc(var(--hero-y) * var(--p2)));
     border-radius: calc(var(--r) * var(--p2));
     overflow: hidden;
   }
   .hero-img .img { transform: scale(calc(1 + var(--p1) * 0.3)); }
   Posições das satélites via um mapa data-x/data-y → top/left/size
   em CSS (grid virtual), como no site auditado.
3. Entrada no load: cada imagem tem data-delay (0, 0.1, 0.2, ...);
   com .is-loaded-a, transitions de opacity + scale revelam a colagem
   em cascata (padrão do Motion 1). Título do hero com o split de
   chars do Motion 3, 2 linhas.
4. Imagens: next/image com fill + sizes; carregue versões @pre
   (placeholder blur) e @2x. As satélites saem do DOM visual no fim
   (opacity via --p1) mas mantêm o espaço.
5. Mobile: reduza a colagem (menos satélites, via CSS/data-mb) e
   diminua a distância de scroll (200vh); gsap.matchMedia se precisar
   de triggers diferentes.
6. prefers-reduced-motion: hero estático em 100vh com o visual central
   já em escala final.

Passo 7 — Seções em série: arquivo, painéis sticky e vídeo-íris

Com a base pronta, estes três motions saem em produção em série — todos reutilizam padrões que você já tem.

Motion 11 — Arquivo: reveals com border draw, sort sticky e filtros

Três comportamentos fazem uma listagem parecer viva. Ao rolar, cada linha divisória se desenha e o card sobe com fade em cascata, com zoom interno nas thumbs ao hover. O cabeçalho de ordenação gruda no topo e, ao colar, encolhe suavemente. E clicar em "Filter" abre um painel que desliza expandindo a altura, com as opções entrando em sequência.

Duas técnicas valem o destaque didático aqui. O sticky que encolhe usa um sentinel de 1px observado por IntersectionObserver — nada de listener de scroll medindo offsets. E o painel que expande usa grid-template-rows: 0fr → 1fr — o jeito moderno de animar altura sem medir um único pixel via JS.

txt
Crie um componente client "ArchiveGrid" em Next.js:

1. Grid responsivo (1 → 3/4 colunas) de cards: thumb (next/image em
   moldura overflow-hidden rounded), título, tags-pílula (GhostButton
   do Motion 8 em versão mini).
2. Reveals: cada card com data-shown via useInView (once). Linha
   divisória: .border { overflow: hidden; height: 1px }
   .border > div { height: 100%; background: currentColor;
   transform: translateY(-110%); transition: transform .8s
   var(--ease-power2-out); } [data-shown] .border > div
   { transform: translateY(0); } Título/texto com o Motion 3.
3. Hover: thumb com img scale(1.05), transition .7s ease-out
   (group-hover); cursor "View" (data-cursor do Motion 9).
4. Header de ordenação sticky:
   - position: sticky; top: 0; background igual ao da página; z-index
     acima do grid.
   - Sentinel de 1px acima do header observado por IntersectionObserver;
     quando sai do viewport → classe 'is-stuck' no header →
     .title { transform: scale(0.8); transform-origin: left bottom;
     transition: transform var(--a-global-t-s) var(--ease-power4-out) }
     e paddings reduzidos (via transform/margins, não height).
5. Painel de filtros (slide toggle):
   - Wrapper display: grid; grid-template-rows: 0fr;
     transition: grid-template-rows .6s var(--ease-power4-out);
     aberto → 1fr. Filho com min-height: 0; overflow: hidden.
   - Seta do botão rotaciona 90°; opções entram com stagger (Motion 3).
   - aria-expanded/aria-controls no botão.
6. Aplicar filtro: atualize a lista (searchParams/estado) e re-dispare
   os reveals apenas dos cards novos; ScrollTrigger.refresh() após o
   reflow.

Motion 12 — Painéis sobre background sticky com swap por hover

Uma imagem grande fica fixa ao fundo enquanto cartões brancos deslizam por cima dela conforme você rola — a imagem parece parada no tempo atrás dos painéis, que passam como páginas. E dentro do painel de categorias, pairar sobre cada nome troca a imagem de fundo em fade.

A lição deste motion: o efeito básico não usa nenhum JS. O fundo é position: sticky; top: 0; height: 100vh num wrapper alto; os painéis são blocos normais com border-radius e z-index maior que rolam por cima. Stacking + sticky, só isso. O swap por hover reutiliza o mecanismo .now/.prev que você verá no Motion 10.

txt
Crie um componente client "StickyShowcase" em Next.js:

1. Estrutura:
   <section class="showcase">           <!-- altura: conteúdo -->
     <div class="showcase-bg">          <!-- sticky top-0 h-screen,
                                             z-index: 1 -->
       <div class="bg-layer now" data-target="slug-1">…img…</div>
       <div class="bg-layer" data-target="slug-2">…img…</div>
       <!-- 1 camada por categoria, todas pré-montadas -->
     </div>
     <div class="showcase-panels">      <!-- z-index: 2, margin-top:
                                             -100vh para sobrepor -->
       <div class="panel">…título + destaque…</div>
       <div class="panel">…lista de categorias…</div>
     </div>
   </section>
2. Painéis: background branco, border-radius no topo
   (var(--r) var(--r) 0 0), height ~85-100vh cada, conteúdo
   distribuído (flex column space-between). O efeito de "passar por
   cima" é só stacking + sticky, sem JS.
3. Swap por hover: itens da lista com data-target; mouseenter troca
   .now/.prev nas bg-layers (mesmo CSS de crossfade do Motion 10).
   Sem hover ativo, mantenha a última categoria ou a default.
4. Reveals internos dos painéis com o Motion 3 (data-shown/useInView).
5. Ao clicar numa categoria: navegue com o TransitionLink (Motion 2).
6. Mobile: hover não existe; as camadas trocam por scroll (qual item
   da lista está no centro) ou fixam a default. Valide o custo das
   camadas montadas (limite N; lazy para além da dobra).

Motion 13 — Vídeo com expansão por clip-path

O vídeo não começa em tela cheia: surge como uma janela recortada com cantos arredondados dentro da seção. Conforme você rola, o recorte se abre continuamente até o vídeo tomar a viewport inteira — uma íris de câmera acoplada à rolagem. É o motor do Passo 2 aplicado a uma única propriedade: clip-path: inset(... round ...) consumindo --p1.

txt
Crie um componente client "ClipVideoSection" em Next.js:

1. Estrutura: section[data-scroll-trigger data-start="top bottom"
   data-end="top top"] > div.video-full (height: 100vh) > video +
   overlay opcional.
2. CSS da íris (consumindo --p1 do Motion 4; --p1 vai de 0 quando a
   seção entra a 1 quando cola no topo):
   .video-full {
     will-change: clip-path;
     clip-path: inset(
       calc(var(--p2) * 20%) calc(var(--p2) * 15%)
       calc(var(--p2) * 20%) calc(var(--p2) * 15%)
       round calc(var(--r) * var(--p2) * 4)
     );
   }
   (em --p1: 1 o inset zera e o raio zera: fullscreen puro)
   Toggle will-change apenas enquanto a seção está no viewport.
   Fechamento na saída (opcional): segundo elemento data-scroll-trigger
   interno com data-start="bottom bottom" data-end="bottom top" e um
   segundo par de vars aplicando o inset inverso.
3. Vídeo: autoPlay muted loop playsInline preload="metadata" poster;
   sources webm antes de mp4; muted reforçado via ref em useEffect
   (issue conhecida do React) + .play().catch(() => {}).
   Versão mobile mais leve via <source media> ou swap por matchMedia.
4. Pausa fora do viewport (IntersectionObserver → video.pause()/play())
   para economizar bateria/CPU.
5. prefers-reduced-motion: sem íris (clip-path none, aspect-ratio 16/9
   estático) e vídeo pausado exibindo o poster.

Passo 8 — Polish de desktop: cursor contextual e preview flutuante

Camada de acabamento, desktop apenas. Nos dois casos, a regra é a mesma: renderize somente em (hover: hover) and (pointer: fine) — em touch, os componentes retornam null e nada quebra.

Motion 9 — Cursor customizado com labels contextuais

Além do cursor do sistema, um pequeno ponto acompanha o mouse com atraso elástico. Sobre uma imagem clicável, ganha o rótulo "View"; sobre uma ampliável, "Zoom"; um halo desfocado acompanha por trás. O cursor vira um guia de affordance — ele conta o que cada área faz antes do clique.

A forma idiomática do movimento é gsap.quickTo em x/y (a única exceção justificada ao "CSS anima": seguir o mouse exige JS mesmo). E uma regra de acessibilidade que o site auditado respeita: nunca cursor: none. O ponto é um companheiro do cursor nativo, não um substituto.

txt
Crie um componente client "CustomCursor" em Next.js:

1. Estrutura fixa (root do layout): div.cursor (fixed, top-0 left-0,
   pointer-events-none, z-max) > div.cursor-body (ponto) +
   div.cursor-label[data-type=view] + div.cursor-label[data-type=zoom]
   + div.cursor-blur (halo com filter: blur).
2. Movimento: dentro de useGSAP,
   const xTo = gsap.quickTo(el, 'x', { duration: 0.4,
     ease: 'power3.out' });
   const yTo = gsap.quickTo(el, 'y', { duration: 0.4,
     ease: 'power3.out' });
   window mousemove → xTo(e.clientX); yTo(e.clientY).
   O halo blur usa duration maior (0.8) para "arrastar" atrás.
3. Contexto: listener delegado de mouseover/mouseout no document;
   target.closest('[data-cursor="view"|"zoom"]') define o rótulo
   ativo (classe no cursor); os rótulos entram/saem pelo padrão de
   máscara do Motion 3.
4. Marque as áreas no site: thumbs de listagem → data-cursor="view";
   imagens ampliáveis (Motion 15) → data-cursor="zoom".
5. Renderize o componente apenas em (hover: hover) and (pointer: fine)
   (matchMedia num useEffect; retorne null em touch). NUNCA esconda o
   cursor nativo (cursor: none prejudica acessibilidade); o ponto é um
   companheiro, não um substituto.
6. prefers-reduced-motion: siga o mouse sem lerp (posição direta) ou
   desative o componente.

Motion 10 — Preview flutuante em hover de lista

Uma lista aparentemente simples de nomes. Ao passar o mouse sobre um, uma miniatura brota flutuando ao lado do cursor, com leve zoom-out de assentamento. Deslize para o próximo nome e a imagem se troca em crossfade. Percorrer a lista vira folhear um catálogo invisível.

O segredo do crossfade contínuo é o par de classes .now/.prev: a imagem anterior fica um instante visível por baixo enquanto a nova assenta por cima. E todas as miniaturas são pré-renderizadas e empilhadas desde o início — o hover nunca espera rede.

txt
Crie um componente client "HoverPreview" em Next.js:

1. Estrutura: div.popup (fixed top-0 left-0, pointer-events-none) >
   div.popup-frame (tamanho fixo, ex. 200x200, rounded, overflow
   hidden, opacity 0) > N divs.preview[data-target=slug], cada uma
   absolute inset-0 com next/image (todas montadas desde o início:
   o hover nunca espera rede).
2. CSS de troca (crossfade com assentamento):
   .preview { opacity: 0; transform: scale(1.05);
              transition: opacity .3s, transform 0s; }
   .preview.now { opacity: 1; transform: scale(1); z-index: 2;
                  transition: opacity .3s, transform 1.2s; }
   .preview.prev { opacity: 0; transform: scale(1); z-index: 1;
                   transition: opacity 0s .3s, transform 0s 1.2s; }
   (a .prev segura a imagem anterior visível por baixo durante o fade
   da nova; é isso que faz a troca parecer contínua)
3. Lógica: mouseenter num <li data-target=slug> → a preview
   correspondente vira .now, a antiga vira .prev; mouseenter na lista →
   frame opacity 1; mouseleave da lista → opacity 0 (após transição,
   limpe .now/.prev).
4. Movimento: siga o mouse com gsap.quickTo (offset de ~20px do
   cursor, duration 0.5, power3.out).
5. Desktop apenas ((hover: hover)); em touch a lista funciona sem
   preview. Os itens seguem linkáveis e acessíveis por teclado
   (preview é decorativa, aria-hidden).

Passo 9 — A página de detalhe: split-scroll, zoom FLIP e relacionados

O conjunto mais interdependente do site — implemente os três juntos.

Motion 14 — Split-scroll com aside fixa

A página de projeto se divide em duas: à direita, um painel com título, ficha técnica e descrição que fica parado; à esquerda, a coluna de fotos desliza verticalmente conforme você rola, com a foto ativa destacada e um indicador circular de progresso no canto. A sensação de folhear o portfólio com uma mão enquanto a ficha permanece à vista.

De novo o motor do Passo 2: o wrapper alto define o comprimento do scroll, a coluna de mídia é position: fixed movida por transform: translateY(calc(-50% * var(--p1))), e até o indicador circular é a mesma var — stroke-dashoffset calculado de --p2 num SVG.

txt
Crie um componente client "ProjectDetail" em Next.js:

1. Estrutura:
   <div class="detail" data-scroll-trigger data-start="top top"
        data-end="bottom bottom">        <!-- altura: N * espaço/foto -->
     <div class="media-col">             <!-- fixed left, w-1/2 -->
       <ul>…N figuras (imagens/vídeos)…</ul>
     </div>
     <aside class="info">…título, ficha (dl), descrição…</aside>
                                         <!-- fixed right, w-1/2,
                                              h-screen -->
     <div class="progress">…indicador circular…</div>
   </div>
2. CSS: .media-col { position: fixed; top: 0; height: 100%;
   transform: translateY(calc(-50% * var(--p1))); }
   (a lista tem o dobro da altura da viewport de percurso; --p1 cobre
   o trajeto todo). Indicador circular: stroke-dashoffset =
   calc(circunferência * var(--p2)) num SVG.
3. Item ativo: derive de --p1 (onUpdate do trigger já roda; num
   callback adicional, calcule o índice e aplique classe ._on ao item:
   fundo do placeholder some, cursor "Zoom" habilita).
4. A aside é fixed com pointer-events: none no wrapper e auto nos
   interativos (títulos, links, botão copiar link), para o scroll
   "atravessar" o painel.
5. Botão "copiar link" com flip de label Copy → Copied (padrão do
   Motion 8; estado _ok temporário via clipboard API).
6. Mobile: desative o layout split (media-col relativa, aside após o
   conteúdo, progresso oculto), via CSS/matchMedia; é o mesmo fallback
   do site auditado.
7. Integre com o Motion 15 (zoom) e finalize com o Motion 16
   (relacionados horizontais).

Motion 15 — Click-to-zoom de imagem (FLIP)

Clique numa foto e ela cresce suavemente do lugar onde está até o centro da tela, enquanto o resto da página escurece e o scroll trava. Clicar de novo faz a foto voltar encolhendo exatamente para o buraco de onde saiu. Nada de modal que pisca: é a própria imagem viajando.

O nome da técnica é FLIP (First-Last-Invert-Play): medir o retângulo atual, calcular o transform até o retângulo final e animar só transform. O plugin Flip do GSAP (gratuito) faz isso nativamente.

txt
Implemente o click-to-zoom em Next.js com GSAP Flip:

1. Registre o plugin Flip em lib/gsap.ts. Imagens ampliáveis envoltas
   em <figure class="zoomable" data-cursor="zoom">.
2. Ao clicar:
   - const state = Flip.getState(figure);
   - Aplique a classe 'is-zoomed' (CSS posiciona a figura fixed,
     centralizada, max 90vw/90vh, object-fit contain, z-index alto);
   - Flip.from(state, { duration: 0.8, ease: 'power4.inOut',
     absolute: true });
   - Marque <html> com 'is-zoom-in': CSS aplica opacity .3 e
     pointer-events: none no restante; um backdrop fixo captura o
     clique de fechar; lenis.stop().
3. Fechar (clique/Escape): Flip de volta ao estado original
   (getState do zoomed → remove classes → Flip.from), depois
   lenis.start() e foco devolvido à figura.
4. Carregue a versão @2x da imagem ao ampliar (troque o src num
   onStart; mantenha a @1x até a nova decodificar para não piscar).
5. Acessibilidade: role="button" + aria-expanded na figura, Escape
   fecha, foco gerenciado; prefers-reduced-motion troca o FLIP por
   fade rápido.

Motion 16 — Lista horizontal com drag

No fim da página de detalhe, uma fileira de cartões corre horizontalmente: arraste com o mouse ou o dedo e os cartões deslizam. É a prateleira final que emenda uma navegação na outra.

A opção fiel ao site auditado dispensa bibliotecas: contêiner com overflow-x: auto, scrollbar oculta, drag-to-scroll com Pointer Events convertendo deltaX em scrollLeft, e data-lenis-prevent para o Lenis global não capturar o gesto.

txt
Crie um componente client "RelatedRow" em Next.js:

1. Estrutura: div.row (overflow-x auto, scrollbar oculta via
   scrollbar-width: none e ::-webkit-scrollbar { display: none },
   data-lenis-prevent) > ul flex com N cards (flex 0 0 auto,
   width ~40vw desktop / 80vw mobile, gap).
2. Drag-to-scroll com Pointer Events:
   pointerdown → guarde startX/scrollLeft e setPointerCapture;
   pointermove → row.scrollLeft = scrollLeft - (x - startX);
   pointerup → release. Durante o drag, classe 'is-dragging'
   (cursor grabbing) e suprima o clique se houve movimento > 8px
   (preventDefault no click subsequente).
3. Inércia: o scroll nativo já dá momentum em touch; para mouse,
   aplique um decay simples no pointerup (velocity * fricção via
   requestAnimationFrame) ou aceite a parada seca (aceitável).
4. Cards: thumb com hover zoom (Motion 11), título com Motion 3,
   cursor "View" (Motion 9). Primeiro/último item com padding lateral
   var(--go) para respiro.
5. Acessibilidade: a lista é navegável por Tab normalmente (scroll
   nativo acompanha o foco); setas de teclado opcionais.
6. Se o design pedir snap/paginação/autoplay: troque por Embla
   Carousel mantendo o restante igual.

Passo 10 — Scroll infinito nas galerias (Motion 6)

Depois da página de detalhe, ela ganha um superpoder raro: nas galerias de um item, a rolagem nunca acaba. Chegou na última imagem? A primeira reaparece emendada, em loop infinito, como um rolo contínuo de tecido.

A técnica: uma segunda instância de Lenis com infinite: true, montada num wrapper próprio, com o conteúdo duplicado para o loop fechar. E um cuidado que não é opcional: scroll infinito é hostil a teclado e leitores de tela — o fallback de lista estática precisa existir.

txt
Implemente o scroll infinito de galeria em Next.js:

1. Componente client "InfiniteGallery" usado nas rotas de detalhe:
   - Estrutura: div.wrapper (viewport, overflow hidden) > div.content
     contendo a lista de imagens DUPLICADA (duas cópias idênticas,
     para o loop emendar).
   - const lenis = new Lenis({ lerp: 0.1, wrapper: wrapperRef.current,
     content: contentRef.current, infinite: true });
   - rAF próprio (gsap.ticker.add com função nomeada) e cleanup em
     3 passos como no Prompt 0. PAUSE o Lenis global da página
     (useLenis().stop()) enquanto esta instância domina o scroll,
     e restaure no unmount.
2. No callback lenis.on('scroll'), normalize o progresso
   (scroll % alturaDeUmaCopia) e escreva --p1 no content para efeitos
   dependentes de posição (ex.: item central destacado).
3. Barra/indicador de progresso circular fixo lendo o mesmo progresso.
4. Acessibilidade: infinite scroll confunde navegação por teclado;
   ofereça lista estática alternativa (prefers-reduced-motion e
   navegação por Tab desativam o modo infinito, caindo para scroll
   normal com a lista única).
5. Mobile: valide o comportamento de touch; se necessário, desative
   infinite e use scroll nativo (gsap.matchMedia).

Passo 11 — O fechamento: sprites decorativos e o rodapé em três atos

Motion 17 — Sprites decorativos (pop + camadas parallax)

Na página institucional, elementos gráficos da marca brotam na tela — no caso da Plantica, literalmente flores: cada uma puxa do nada com um pop elástico (nasce em escala zero e assenta no tamanho final), em posições e tempos diferentes — um jardim florescendo em stop-motion ao redor do texto. Outros ficam em camadas de fundo que se movem em velocidades diferentes durante a rolagem, dando profundidade de diorama.

A sacada de performance: tudo vem de sprite sheets — uma imagem única com todos os recortes, cada elemento exibido via background-position com CSS vars. Dezenas de elementos decorativos, um único request.

txt
Crie um componente client "SpriteDecor" em Next.js:

1. Gere a sprite sheet dos elementos (uma .webp com todos os recortes)
   e um JSON de manifesto: [{ name, fx, fy, w, h, x, y, layer,
   delay }] (posição no sheet + posição/camada na tela).
2. Componente:
   <div class="sprite" data-type="pop|bg" data-scroll-trigger>
     {frames.map(f =>
       <div class="frame" style={{ '--fx': f.fx, '--fy': f.fy,
            '--sw': SW, '--sh': SH, '--w': f.w, '--h': f.h,
            left: f.x, top: f.y, '--d': `${f.delay}s` }}>
         <div class="img" />
       </div>)}
   </div>
   CSS: .frame .img { width: calc(var(--w) * 1px);
     height: calc(var(--h) * 1px);
     background-image: url(sprite.webp);
     background-position: calc(var(--fx) * -1px) calc(var(--fy) * -1px);
     background-size: calc(var(--sw) * 1px) calc(var(--sh) * 1px); }
   (no site auditado as contas usam unidade vw para escalar com a tela;
   parametrize a unidade)
3. Tipo pop: .img { transform: scale(0); transition: transform 1.8s
   var(--d) var(--ease-power4-out) } → [data-shown] .img
   { transform: scale(1) } (dispare com useInView; o easing out
   longo dá o assentamento "elástico").
4. Tipo bg: cada camada num wrapper com
   transform: translateY(calc(var(--p1) * var(--depth) * -10vh))
   (depth 0.3/0.6/1.0 por camada), consumindo o motor do Motion 4.
5. Todos os frames com pointer-events: none e aria-hidden (decoração).
6. prefers-reduced-motion: pops entram já em scale(1) com fade;
   parallax congelado.

Motion 18 — Rodapé composto: parallax reveal, marquee e canvas de blur

Três atos no mesmo lugar. Primeiro, o rodapé não rola para dentro: ele é revelado — a página termina e o conteúdo desliza para cima como uma cortina, mostrando o rodapé que já estava embaixo, movendo-se em velocidade reduzida. Segundo, uma faixa-convite desliza continuamente, estilo letreiro. Terceiro, atrás da faixa, uma mancha de cor desfocada segue o mouse como luz atravessando vidro fosco — um canvas WebGL de poucos KB.

Repare que cada ato usa a ferramenta mínima necessária: o reveal é o motor de scroll de sempre; o marquee é CSS puro num Server Component, zero JS; só a mancha de luz justifica WebGL.

txt
Crie o rodapé composto em Next.js (3 sub-componentes):

1. "FooterReveal": footer com
   <div class="footer-body" data-scroll-trigger data-start="top bottom"
        data-end="bottom center">…conteúdo…</div>
   CSS: .footer-body { height: 100vh;
     transform: translateY(calc(var(--p2) * -50vh)); }
   (entra "atrasado" e alcança a posição natural quando o scroll
   termina: efeito de cortina revelando). Corpo da página com z-index
   acima do footer e background sólido.
2. "InviteMarquee" (Server Component, zero JS):
   div.mq (overflow hidden, flex) > ul.mq-ul (flex, width: 200%) >
   2 li.mq-li idênticos (cada um com a frase repetida K vezes);
   CSS: .mq-li { will-change: transform;
     animation: marquee 30s linear infinite; }
   @keyframes marquee { from { transform: translateX(0) }
     to { transform: translateX(-100%) } }
   (com DOIS itens de 100% cada, o -100% emenda perfeito)
   O marquee inteiro é um link para a página de contato; versão
   sr-only do texto fora da faixa; animation: none em
   prefers-reduced-motion; pausa opcional no hover.
3. "MouseBlurCanvas" (client): canvas absoluto atrás do marquee
   (pointer-events: none), com OGL:
   - Plane fullscreen + fragment shader: 2-3 gradientes radiais de
     cores da marca somados, centro deslocado por u_mouse (uniform
     vec2) suavizado com lerp no rAF, u_time para respiração lenta.
   - Resize observer atualiza a resolução; renderer.dispose() e
     cancelamento do rAF no cleanup.
   - Fallback sem WebGL: gradiente CSS estático.
   - Renderize só quando o footer está no viewport
     (IntersectionObserver liga/desliga o rAF).
4. Complete com colunas de sitemap (links com Motion 8), logo grande
   com reveal (Motion 3) e créditos.

As 15 armadilhas que vão te morder (leia antes de codar)

Cada item desta lista custou horas de debug de alguém. Passe por ela antes de escrever a primeira linha — e volte quando algo "misteriosamente" quebrar.

  • "use client" obrigatório em todo componente que toca GSAP, Lenis ou observers.
  • Sempre useGSAP({ scope }) em vez de useEffect cru: o context.revert() automático mata triggers órfãos ao navegar entre rotas.
  • Cleanup do Lenis é em 3 passos: gsap.ticker.remove(update) + lenis.off('scroll', ...) + lenis.destroy(). Vale para a instância global e para as locais (galeria infinita).
  • Trava de scroll com Lenis: use lenis.stop()/start() (menu, zoom), não apenas overflow: hidden; e data-lenis-prevent em áreas com scroll próprio.
  • CSS vars + SSR: inicialize --p1: 0; --p2: 1 no CSS, não só via JS — senão há flash no primeiro paint.
  • transform, opacity e clip-path nas animações por CSS var; nunca anime width, height, top ou left (layout thrashing a cada frame de scroll).
  • will-change cirúrgico: só nos elementos que animam, de preferência togglado quando a seção está visível; will-change global degrada memória de GPU.
  • ScrollTrigger.refresh() após load de imagens e fontes e após cada transição de rota; posições calculadas antes do layout final ficam erradas.
  • Split de texto só depois das fontes (autoSplit: true ou document.fonts.ready); e sempre aria-label no pai + aria-hidden nos spans.
  • gsap.matchMedia() para variações responsivas de trigger; ScrollTrigger.matchMedia está deprecado desde o GSAP 3.11.
  • Estados globais no <html> (is-loaded, is-leaving, is-menu-opened, is-zoom-in): centralize num pequeno store que espelha as classes; nunca toggles espalhados sem dono.
  • Cursor custom e previews: desktop apenas ((hover: hover) and (pointer: fine)); em touch, os componentes retornam null. E nunca cursor: none.
  • WebGL: dispose sempre (renderer, geometrias, texturas) e rAF cancelado no unmount; renderize apenas com o canvas no viewport.
  • Infinite scroll é hostil a teclado e leitores de tela: ofereça sempre o fallback de lista estática.
  • prefers-reduced-motion em tudo: o padrão CSS-first facilita — um bloco único de media query pode neutralizar transitions e keyframes globalmente.

Por onde começar hoje

Se o guia inteiro parece muito, comece pelo trio que paga o investimento mais rápido: o Passo 1 (a fundação — sem ela nada funciona), o Passo 2 (o motor --p1/--p2 — meia hora de trabalho que destrava seis motions) e o Motion 8 (botões e underlines — CSS puro, resultado imediato, usado em todo lugar).

Com esses três no lugar, cada motion seguinte é incremental: você já tem os estados, o motor e a linguagem. Vale também abrir o plantica.net numa aba ao lado e rolar devagar, reconhecendo cada padrão deste guia em ação no original — não há exercício melhor para treinar o olho.

E a filosofia fica no músculo: o JS escreve variáveis e classes; o CSS faz o movimento. É ela — mais do que qualquer biblioteca — que separa os sites que parecem premium dos que só têm animação.

Leia também