Pular para o conteúdo
Zumkai

Motions premium em Next.js: os 17 padrões do site da InterGest Canada, passo a passo

GSAP, Framer Motion e Embla no mesmo site sem brigar: os 17 padrões de motion da InterGest Canada em Next.js, com o prompt de implementação pronto de cada um.

  • motion
  • gsap
Home do site da InterGest Canada: título “Your Gateway to Canadian Expansion” em branco sobre vídeo aéreo de Toronto com a CN Tower
Sumário
  1. A filosofia: cada ferramenta no seu território
  2. A stack e o papel de cada peça
  3. O mapa dos 17 motions
  4. Passo 1 — A fundação: infraestrutura de animação
  5. Passo 2 — Vitórias rápidas: marquee, botões CTA e banner de cookies
  6. Passo 3 — O cartão de visitas: hero completo sobre vídeo
  7. Passo 4 — Produção em série: os quatro reveals de scroll
  8. Passo 5 — Os trilhos: timeline horizontal e carrossel numerado
  9. Passo 6 — A mais delicada: seção pinada com troca de steps (Motion 8)
  10. Passo 7 — A joia da coroa: rotação por frame sequence (Motion 13)
  11. Passo 8 — O fechamento: formulário, idiomas e o menu overlay
  12. As 11 armadilhas que vão te morder (leia antes de codar)
  13. Por onde começar hoje

Quinto estudo de engenharia reversa da série — e depois de quatro arquiteturas autorais (Plantica, ANAI, Studio Modular e Beetogreen), este guia responde uma pergunta diferente: como fica o kit clássico dos sites premium quando bem executado? Qual biblioteca faz o quê, e onde passa a fronteira entre elas?

O auditado é a InterGest Canada (intergestcanada.com) — "Your Gateway to Canadian Expansion": a operação canadense da rede internacional InterGest, que cuida de tudo que uma empresa estrangeira precisa para operar no Canadá, da abertura à gestão diária. O site abre com um voo aéreo sobre Toronto (a CN Tower cortando o título), tipografia branca gigante sobre vídeo, e uma linguagem de motion corporativa e polida — construída em Webflow, o que torna o exercício de tradução para Next.js especialmente instrutivo. Os 17 padrões abaixo são agnósticos de conteúdo: nomes, valores e assets são placeholders. O que você leva é o padrão, a técnica e um prompt pronto para colar no seu agente de IA.

Como sempre na série, a ordem dos passos é a ordem de construção.

A filosofia: cada ferramenta no seu território

Os quatro guias anteriores tinham teses radicais (CSS-first, três camadas, gates, estados). Este ensina o arranjo mais comum — e a lição está em traçar bem as fronteiras:

  • GSAP é a espinha dorsal do espetáculo: reveals, timelines do hero, split text, pin, scrub, contadores, parallax e o frame sequence. Tudo que é coreografia de página passa por ele.
  • Framer Motion cuida do ciclo de vida da UI: menu overlay, banner de cookies, estados de formulário, física de mola nos botões. O motivo é um só e vale decorar: AnimatePresence resolve animação de desmontagem, que é dolorosa em GSAP puro. Componente que entra E sai → Framer; coreografia de scroll → GSAP.
  • Embla Carousel para tudo que desliza em trilhos: timeline horizontal e carrossel numerado. Headless, leve, acessível.
  • CSS puro onde nenhum JS se justifica: o marquee do rodapé roda no compositor sem uma linha de script.
  • Lenis é opcional e declarado como tal: uma escolha de feel, não um requisito dos motions.

A stack e o papel de cada peça

BibliotecaPapel no projetoPor quê
GSAP 3.13+ e @gsap/reactEspinha dorsal: reveals, timelines, split text, pin/scrubGSAP é 100% gratuito, incluindo todos os plugins (SplitText, ScrollTrigger, Draggable), desde a aquisição pela Webflow
ScrollTrigger (plugin GSAP)Todas as animações disparadas/controladas por scrollPin de seções, scrub, reveals em cascata
LenisSmooth scroll global (opcional)Dá o "deslizar amanteigado" típico de sites premium; integra nativamente com ScrollTrigger. É escolha de feel, não requisito: se o projeto pedir scroll nativo, pule o Lenis
Framer Motion (motion)Micro-interações de UI: menu overlay, banner de cookies, estados de formulário, hover de botõesAnimatePresence resolve montagem/desmontagem de componentes, que é doloroso em GSAP puro. O pacote atual chama-se motion (Motion for React)
Embla CarouselTimeline horizontal e carrossel de conteúdoLeve, headless, acessível, com API de dots/setas customizados

O mapa dos 17 motions

#MotionUso típicoGatilho
1Menu overlay fullscreenHeader (hambúrguer)Clique
2Banner de cookiesGlobal (1ª visita)Load
3Reveal do título hero (split text)HeroLoad
4Entrada sequencial do subtítulo/CTAHeroLoad
5Vídeo de fundo em loopHeroAutomático
6Timeline horizontal navegávelSeção de história/etapasSetas/arraste
7Cards em cascata + hover zoomGrid de cards com fotoScroll + hover
8Seção pinada com troca de stepsSeção de processoScroll
9Contadores numéricos animadosGrid de estatísticasScroll
10Reveal de blocos de textoBlocos de argumento/vantagemScroll
11Carrossel com paginação numeradaSeção de serviços/featuresClique/auto
12Parallax multicamadas de imagensSeção editorial com fotosScroll
13Rotação por frame sequenceObjeto 3D "fake" (globo, produto)Auto/scroll
14Marquee infinitoRodapé/faixas decorativasLoop automático
15Estados animados de formulárioNewsletter/contatoEnvio
16Hover de botões CTABotões globaisHover
17Seletor de idioma + fade de rotaHeaderHover/clique

Reveals genéricos de seção (headings e parágrafos que sobem com fade ao entrar no viewport) não têm número próprio: reutilize o padrão dos Motions 7 e 10 (y + autoAlpha com ScrollTrigger once).

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

O setup padrão da casa: Lenis sincronizado ao ScrollTrigger com o cleanup em três passos (a causa número 1 de vazamentos quando esquecido), plugins registrados uma única vez, e o hook de prefers-reduced-motion desde o início.

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 embla-carousel-react
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 raf() de uma
                                          // instância destruída (leak)
       lenis.off('scroll', ScrollTrigger.update);
       lenis.destroy();
   - Exponha a instância via React Context com um hook useLenis(), para
     outros componentes poderem chamar lenis.stop() / lenis.start()
     (o menu overlay do Motion 1 depende disso).
3. Registre os plugins uma única vez num arquivo lib/gsap.ts:
   gsap.registerPlugin(ScrollTrigger, SplitText, useGSAP) e exporte gsap.
   Esse arquivo só deve ser importado por componentes client.
4. Envolva {children} no layout.tsx raiz com <SmoothScrollProvider>.
5. Crie um hook usePrefersReducedMotion() que lê a media query
   (prefers-reduced-motion: reduce); todos os componentes de animação devem
   pular ou reduzir animações quando true.
Entregue os arquivos completos e o layout.tsx atualizado.

Passo 2 — Vitórias rápidas: marquee, botões CTA e banner de cookies

Três componentes simples que estabelecem o vocabulário — e já demonstram a divisão de territórios: CSS para o marquee, Framer Motion para o que nasce e morre.

Motion 14 — Marquee infinito

Uma faixa de texto que desliza continuamente, como o letreiro de chegadas de um aeroporto. CSS puro, Server Component, zero JS — e a matemática do loop invisível: o track contém dois grupos idênticos e anima translateX(0 → -50%); quando a primeira cópia sai, a segunda está exatamente no lugar dela.

txt
Crie um componente "FooterMarquee" em Next.js (pode ser Server Component —
não precisa de JS):

1. Estrutura:
   <div class="overflow-hidden whitespace-nowrap" aria-hidden="true">
     <div class="marquee-track inline-flex">
       <!-- GRUPO A: a frase repetida K vezes (ex.: 8x, via Array(K).map) -->
       <span>SUA FRASE AQUI&nbsp;·&nbsp;</span> ...
       <!-- GRUPO B: cópia idêntica do grupo A -->
     </div>
   </div>
   A regra é: o track contém DOIS grupos idênticos, cada um com
   exatamente 50% do conteúdo — é isso que faz a matemática do -50%
   fechar perfeitamente.
2. CSS (globals.css ou tailwind config):
   .marquee-track { animation: marquee 28s linear infinite; }
   @keyframes marquee {
     from { transform: translateX(0); }
     to   { transform: translateX(-50%); }
   }
   will-change: transform no track.
3. Pause opcional no hover: .marquee:hover .marquee-track
   { animation-play-state: paused; }
4. @media (prefers-reduced-motion: reduce) { .marquee-track
   { animation: none; } }
5. Acessibilidade: aria-hidden no marquee decorativo e uma versão do
   texto acessível (sr-only) fora dele.
6. Tipografia grande (text-4xl+), cor sutil (ex. texto vazado com
   -webkit-text-stroke ou opacidade baixa), gap consistente entre
   repetições.

Motion 16 — Micro-interações de hover nos botões CTA

Ao passar o mouse, o botão responde: o fundo sobe deslizando por dentro (um span absoluto revelado no hover), a setinha desloca alguns pixels apontando o caminho, e o clique "afunda" o botão com física de mola. E um detalhe de integração que vale o prompt inteiro: motion.create(Link) — o jeito certo de dar whileTap a um link interno sem perder o prefetch do App Router.

txt
Crie um componente "CtaButton" reutilizável em Next.js:

1. Props: href, children, variant: 'solid' | 'outline', external?: boolean.
2. Base (Tailwind): inline-flex items-center gap-2 rounded-full px-7 py-3.5
   relative overflow-hidden group.
3. Efeito "fundo que sobe": <span> absolute inset-0 na cor de hover com
   translate-y-full transition-transform duration-500
   ease-[cubic-bezier(0.76,0,0.24,1)] group-hover:translate-y-0;
   o label fica em <span class="relative z-10"> e troca de cor com
   transition-colors sincronizada.
4. Seta: ícone → com transition-transform duration-300
   group-hover:translate-x-1.5.
5. Elemento raiz animado:
   - Rotas INTERNAS: use next/link com Framer Motion:
     const MotionLink = motion.create(Link)  // motion(Link) em versões
                                             // anteriores da lib
     <MotionLink href={href} whileTap={{ scale: 0.97 }}
       transition={{ type:'spring', stiffness: 400, damping: 17 }} />
     (preserva prefetch e client-side navigation do App Router)
   - Links EXTERNOS: motion.a com target="_blank" rel="noopener".
6. Estados de foco visíveis (focus-visible:ring) — nunca remova outline
   sem substituir.

Motion 2 — Banner de cookies deslizante

Um segundo e meio depois do site abrir, a caixinha desliza de baixo com mola; aceitar ou rejeitar a faz deslizar de volta — e ela não volta mais. As duas armadilhas de Next.js que o prompt já resolve: localStorage só existe no browser (o componente retorna null até o efeito rodar, evitando hydration mismatch), e o link da política aponta para a rota interna, nunca para staging.

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

1. useEffect verifica localStorage.getItem('cookie-consent'); se ausente,
   seta visible=true após delay de 1500ms (setTimeout com cleanup).
2. Renderize com <AnimatePresence>: motion.div fixed bottom-6 left-6,
   initial { y: 80, opacity: 0 }, animate { y: 0, opacity: 1 },
   exit { y: 80, opacity: 0 }, transition spring
   { stiffness: 260, damping: 24 }.
3. Botões "Aceitar" e "Rejeitar": ao clicar, salvam
   'accepted'/'rejected' no localStorage e setam visible=false
   (o exit anima a saída).
4. Nunca renderize no SSR (retorne null até o efeito rodar) para evitar
   hydration mismatch.
5. Inclua link para a rota INTERNA da política de privacidade do próprio
   projeto (ex.: /legal/privacy-policy), nunca URL de staging ou domínio
   externo.

Passo 3 — O cartão de visitas: hero completo sobre vídeo

O hero da InterGest é o trio clássico executado com precisão: título em split text mascarado, elementos secundários em fila, e o voo sobre Toronto rodando atrás de tudo. Atenção à regra que amarra os Motions 3 e 4: o título anima uma única vez, dentro da timeline única — implementar os dois separados faz o título animar em dobro.

Motion 3 — Reveal do título hero (split text mascarado)

Cada palavra do título gigante sobe de dentro de uma máscara invisível, uma ligeiramente depois da outra, com as palavras-chave em destaque. A API moderna do SplitText (3.13+) faz o trabalho pesado: autoSplit espera as fontes carregarem e re-splita se o layout mudar.

txt
Crie um componente client "HeroTitle" em Next.js com GSAP:

1. Importe gsap e SplitText de lib/gsap.ts (plugins já registrados) e o hook
   useGSAP de @gsap/react.
2. H1 com ref, contendo o texto com <strong> nas palavras de destaque.
   Aplique visibility:hidden inline no H1 para evitar FOUC (flash do texto
   sem animação antes do JS rodar).
3. Dentro de useGSAP (scope no container), use a API moderna do SplitText
   (3.13+) com autoSplit, que espera as fontes carregarem e re-splita
   automaticamente se o layout mudar:
   SplitText.create(h1Ref.current, {
     type: 'lines,words',
     linesClass: 'line',
     mask: 'lines',          // cria wrappers overflow:hidden automáticos
     autoSplit: true,        // aguarda document.fonts.ready internamente
     onSplit: (self) => {
       gsap.set(h1Ref.current, { visibility: 'visible' });
       return gsap.from(self.words, {
         yPercent: 110, duration: 0.9,
         ease: 'power4.out', stagger: 0.06, delay: 0.2
       });                   // retornar o tween faz o autoSplit
     }                       // gerenciar o cleanup sozinho
   });
   (Alternativa sem autoSplit: envolva o split em
   document.fonts.ready.then(() => { ... }) e chame split.revert()
   no cleanup. Nunca faça o split antes das fontes carregarem, senão
   as quebras de linha ficam erradas.)
4. Se prefers-reduced-motion, apenas remova o hidden sem animar.

Motion 4 — Entrada sequencial do subtítulo e CTA

Depois do título, os elementos secundários entram em fila: slogan, parágrafo, botão — cada um com fade subindo, em overlap negativo ('-=0.4') para a fluidez. O olho é guiado de cima para baixo, na ordem que o designer quer.

txt
Estenda o componente HeroTitle para uma seção "Hero" completa:

1. Estruture: h1 (split do motion 3), p.slogan, p.descricao, a.cta — todos
   com classes de seleção dentro do scope do useGSAP.
2. Dentro do onSplit do SplitText (ou após o fonts.ready), crie UMA
   gsap.timeline({ defaults: { ease: 'power3.out' } }):
   - tl.from(split.words, { yPercent: 110, stagger: 0.06, duration: 0.9 })
   - tl.from('.slogan', { y: 30, autoAlpha: 0, duration: 0.6 }, '-=0.45')
   - tl.from('.descricao', { y: 30, autoAlpha: 0, duration: 0.6 }, '-=0.4')
   - tl.from('.cta', { y: 20, autoAlpha: 0, scale: 0.96, duration: 0.5 },
     '-=0.35')
   Retorne a timeline no onSplit para o cleanup automático.
3. As sobreposições negativas ('-=') fazem os elementos entrarem em
   overlap, dando fluidez (nunca espere um terminar 100% para começar
   o próximo).
4. Use autoAlpha (opacity + visibility) em vez de só opacity, para
   elementos invisíveis não capturarem cliques.

Motion 5 — Vídeo de fundo em loop

Por trás do texto, o voo aéreo roda continuamente — cinematografia, não animação programada. <video> nativo com os quatro atributos mágicos, poster, e a issue conhecida do React com muted resolvida via ref.

txt
No componente Hero do Next.js, adicione o vídeo de fundo:

1. Estrutura: section relative h-svh overflow-hidden
   - <video> absolute inset-0 w-full h-full object-cover -z-10 com os
     atributos: autoPlay, muted, loop, playsInline,
     preload="metadata", poster="/hero-poster.webp"
     e <source src="/hero.webm" type="video/webm" /> seguido de
     <source src="/hero.mp4" type="video/mp4" /> (webm antes do mp4:
     o browser usa a primeira source suportada).
   - Overlay: div absolute inset-0 bg-black/40 (legibilidade do texto).
   - Atenção ao stacking: -z-10 pressupõe que a section não tenha
     background próprio cobrindo o vídeo.
2. IMPORTANTE (React): o atributo muted às vezes não é aplicado no SSR
   (issue conhecida do React); garanta via ref em useEffect:
   videoRef.current.muted = true e chame .play().catch(() => {})
   para cobrir políticas de autoplay.
3. Performance: vídeo comprimido em H.264 (mp4) + AV1/VP9 (webm),
   máx ~2-4MB, 1080p, sem faixa de áudio embutida.
4. Acessibilidade: se prefers-reduced-motion, pause o vídeo e mostre
   apenas o poster.

Passo 4 — Produção em série: os quatro reveals de scroll

Quatro motions, um mesmo padrão (y + autoAlpha disparado por ScrollTrigger) em quatro variações — a fábrica de reveals do site.

Motion 7 — Cards em cascata com hover zoom

Os cards de foto não aparecem juntos: ScrollTrigger.batch() os revela em cascata (~120ms entre cada). Depois, o hover dá zoom lento na foto por dentro da moldura — e isso é CSS puro (group-hover:scale-110), porque hover simples não precisa de JS.

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

1. Grid de N cards (grid-cols-1 md:grid-cols-2 xl:grid-cols-4, gap-6).
   Card: figure com overflow-hidden rounded-2xl contendo next/image
   (fill, object-cover), número editorial (01, 02...), h3 e parágrafo.
2. Reveal com GSAP dentro de useGSAP (scope no container):
   ScrollTrigger.batch('.card', {
     start: 'top 85%',
     once: true,
     onEnter: (batch) => gsap.fromTo(batch,
       { y: 60, autoAlpha: 0 },
       { y: 0, autoAlpha: 1, duration: 0.8, ease: 'power3.out',
         stagger: 0.12 })
   })
   Estado inicial via gsap.set('.card', { autoAlpha: 0 }) para não piscar.
3. Hover zoom em CSS: no card use class "group"; na imagem:
   transition-transform duration-700 ease-out group-hover:scale-110.
   No número: transition-colors group-hover:text-[cor-de-marca].
4. Imagens com next/image, sizes corretos e placeholder blur.
5. once:true garante que a cascata rode uma única vez (não repete ao
   rolar para cima e para baixo).

Motion 9 — Contadores numéricos animados

Os números grandes sobem contando do zero ao valor final, como velocímetro chegando à marca. Os detalhes que separam o correto do quebrado: prefix/suffix como strings livres (o mundo real tem "+C$" e "590K"), decimals explícito (uma heurística arredondaria 1.46 para "1.5" — dado errado na tela), tabular-nums para o número não tremer de largura, e o valor final renderizado no HTML do servidor — o buscador indexa o dado real, a animação zera e conta só no client.

txt
Crie um componente client "StatsGrid" em Next.js com contadores GSAP:

1. Dados: array de N itens com o tipo:
   type Stat = {
     label: string;        // nome do indicador
     value: number;        // valor numérico final
     prefix?: string;      // ex.: 'R$', 'C$', '+', '+C$'
     suffix?: string;      // ex.: 'B', 'K', 'M', '%'
     decimals?: number;    // OBRIGATÓRIO quando o valor tem casas
                           // significativas (ex.: 1.46 → decimals: 2)
     description: string;
   };
2. Grid responsivo (2 → 5 colunas). Cada card: label, h3 do número
   (ref via data-attribute), parágrafo.
3. Contador: dentro de useGSAP, para cada card:
   const obj = { val: 0 };
   gsap.to(obj, {
     val: item.value,
     duration: 1.6,
     ease: 'power2.out',
     scrollTrigger: { trigger: el, start: 'top 85%', once: true },
     onUpdate: () => {
       el.textContent = (item.prefix ?? '') +
         new Intl.NumberFormat('en-CA', {
           minimumFractionDigits: item.decimals ?? 0,
           maximumFractionDigits: item.decimals ?? 0
         }).format(obj.val) +
         (item.suffix ?? '');
     }
   });
   (Fixar minimum = maximum evita o número "tremendo" de largura
   durante a contagem; considere também font-variant-numeric:
   tabular-nums no h3.)
4. Reveal dos cards em cascata com ScrollTrigger.batch (stagger 0.08,
   y: 40 → 0, autoAlpha), mesmo padrão do Motion 7.
5. SSR/SEO: renderize o VALOR FINAL formatado no HTML inicial
   (server-friendly) e só zere/anime no client após montar — assim o
   buscador indexa o número real e usuários sem JS veem o dado.
6. prefers-reduced-motion: pule a contagem e mostre o valor direto.

Motion 10 — Reveal de blocos de texto em sequência

Blocos de argumento empilhados, cada um com sua linha divisória que se desenha da esquerda para a direita (scaleX com transform-origin: left) antes do título e do parágrafo entrarem — um trigger individual por bloco, no ritmo do scroll.

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

1. N blocos empilhados; cada bloco: hr decorativa no topo, h3 e parágrafo.
2. Em useGSAP, itere gsap.utils.toArray('.advantage'):
   para cada bloco crie uma timeline com scrollTrigger
   { trigger: bloco, start: 'top 80%', once: true }:
   - Linha: fromTo scaleX 0 → 1, transformOrigin 'left center',
     duration 0.8, ease 'power2.inOut'
   - h3: from { y: 40, autoAlpha: 0 }, duration 0.7, position '-=0.4'
   - p: from { y: 24, autoAlpha: 0 }, duration 0.7, position '-=0.5'
3. Estados iniciais setados via gsap.set para evitar flash.
4. Opcional: transformar em accordion (Radix UI Accordion + animação de
   height com grid-template-rows: 0fr → 1fr em CSS) caso o design final
   peça blocos expansíveis por clique.

Motion 12 — Parallax multicamadas de imagens

Título gigante central e fotos espalhadas que se movem em velocidades diferentes durante o scroll — profundidade de camadas, como a paisagem pela janela do trem. Fator de profundidade declarado por data-speed em cada imagem; ease: 'none' obrigatório (em scrub, a suavização vem do scroll).

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

1. Section relative com min-h-[120vh]; título central enorme em 2 linhas
   (com <strong> em palavras de destaque); 2-4 imagens (next/image)
   posicionadas absolute em pontos assimétricos (ex.: top-left pequena,
   right-center média, bottom-left grande), cada uma com atributo
   data-speed: ex. 0.4, 0.8 e 1.4.
2. Em useGSAP, itere gsap.utils.toArray('[data-speed]') e para cada:
   gsap.to(el, {
     yPercent: -30 * parseFloat(el.dataset.speed),
     ease: 'none',
     scrollTrigger: {
       trigger: sectionRef.current,
       start: 'top bottom',
       end: 'bottom top',
       scrub: true
     }
   });
   ease 'none' é obrigatório em scrub (a "suavização" vem do scroll).
3. O título entra com o padrão SplitText do Motion 3, mas disparado por
   ScrollTrigger (start 'top 75%', once) em vez de load.
4. Compense o deslocamento: posicione as imagens levemente "adiantadas"
   para que fiquem enquadradas quando a seção estiver no centro da tela.
5. Desative o parallax em mobile via gsap.matchMedia('(min-width:768px)')
   — em telas pequenas gera custo de performance sem ganho visual.

Passo 5 — Os trilhos: timeline horizontal e carrossel numerado

Dois carrosséis Embla com personalidades diferentes.

Motion 6 — Timeline horizontal navegável

A história da empresa corre de lado: marcos com número grande e card, navegados por setas ou arraste, com o item ativo em destaque e uma linha decorativa atravessando as estações.

txt
Crie um componente client "HistoryTimeline" em Next.js com Embla Carousel:

1. Instale embla-carousel-react. Dados: array de N marcos
   { label: string, title: string, description: string }
   (label pode ser ano, número de etapa etc.).
2. const [emblaRef, emblaApi] = useEmblaCarousel({ align: 'start',
   dragFree: false, skipSnaps: false })
3. Estrutura: div.embla overflow-hidden (ref) > div.embla__container flex >
   slides com flex: 0 0 70% (desktop) / 0 0 85% (mobile), gap-8.
   Cada slide: label em texto text-7xl font-bold + card com título e texto.
4. Botões Prev/Next chamam emblaApi.scrollPrev()/scrollNext(); desabilite
   (opacity-30, cursor-default) nas extremidades usando
   emblaApi.canScrollPrev()/canScrollNext() sincronizados via evento
   'select' num useEffect (com cleanup do listener).
5. Slide ativo: use selectedScrollSnap() para aplicar classe de destaque
   (label em cor de marca, card com scale 1; inativos com opacity-50).
   Transições via CSS transition-all duration-500.
6. Linha horizontal decorativa atravessando os marcadores (pseudo-elemento
   ou div absoluta) com um ponto em cada estação.
7. Extra: reveal da seção inteira com GSAP ScrollTrigger
   (y:60→0, autoAlpha, once: true) quando entra no viewport.

Motion 11 — Carrossel com paginação numerada

Serviços num carrossel elegante: imagem-conceito fixa de um lado, painéis de texto trocando do outro, marcadores 01/02/03 que acendem, autoplay opcional — e a micro-entrada do texto a cada troca via key={selectedIndex} num motion.div.

txt
Crie um componente client "NumberedCarousel" em Next.js com Embla:

1. Instale embla-carousel-react e embla-carousel-autoplay.
2. Dados: N itens { index: '01', title, description }.
3. useEmblaCarousel({ loop: true }, [Autoplay({ delay: 6000,
   stopOnInteraction: true })]). Autoplay é opcional: exponha via prop.
4. Layout: grid 2 colunas — esquerda: imagem-conceito fixa (next/image)
   que NÃO troca; direita: viewport do Embla com os N painéis de texto.
   (Variação: se cada item tiver imagem própria, sincronize a troca da
   imagem com o evento 'select'.)
5. Navegação inferior: setas prev/next + fileira de marcadores numerados
   gerados de scrollSnapList(); clique chama scrollTo(i).
   Marcador ativo: cor de marca + underline animado
   (after:scale-x-0 → after:scale-x-100 com transition, origin-left);
   inativos: opacity-40.
6. Sincronize o índice ativo com o evento 'select' do emblaApi em
   useEffect (com cleanup do listener).
7. Micro-motion interna: quando o slide muda, anime o h3 e o parágrafo
   do painel ativo com Framer Motion (key={selectedIndex} em um
   motion.div com initial { y: 24, opacity: 0 } → animate { y:0,
   opacity:1 }, duration 0.5) para o texto "entrar" a cada troca.
8. Acessibilidade: aria-label nas setas, role="tablist"/"tab" nos dots,
   pausa do autoplay em hover/focus.

Passo 6 — A mais delicada: seção pinada com troca de steps (Motion 8)

Ao chegar na seção de processo, a tela trava: você continua rolando, mas a seção fica presa — a imagem-conceito fixa de um lado enquanto os steps se revezam do outro, no ritmo exato da sua rolagem. Como folhear um livro cuja capa não sai do lugar.

É a técnica canônica de pinned sections (pin: true + scrub + timeline mapeando o progresso), e a mais delicada do catálogo: teste bem a sincronização com o Lenis, use gsap.matchMedia() para o fallback mobile (sem pin), e chame ScrollTrigger.refresh() depois das imagens carregarem.

txt
Crie um componente client "ProcessSection" em Next.js com GSAP ScrollTrigger:

1. Layout: section > div.wrapper (grid 2 colunas; esquerda = imagem-conceito
   com next/image; direita = div.steps relative onde os N painéis de step
   ficam empilhados com position absolute inset-0).
   Opcional: uma coluna/área extra de links relacionados (ex.: lista de
   serviços + botão "ver mais"), se o design pedir; ela fica fora da área
   animada dos steps.
2. Cada painel: label "Step N", h3 e lista de bullets.
   Estado inicial: painel 1 visível; demais com autoAlpha 0 e y 40.
3. Dentro de useGSAP, envolva TUDO em gsap.matchMedia():
   const mm = gsap.matchMedia();
   mm.add('(min-width: 1024px)', () => {
     const tl = gsap.timeline({
       scrollTrigger: {
         trigger: wrapperRef.current,
         start: 'top top',
         end: '+=200%',            // ~2 viewports de rolagem "presa";
                                   // ajuste proporcional ao nº de steps
         pin: true,
         scrub: 0.8,
         anticipatePin: 1
       }
     });
     tl.to('.step-1', { autoAlpha: 0, y: -40, duration: 1 })
       .fromTo('.step-2', { autoAlpha: 0, y: 40 },
                          { autoAlpha: 1, y: 0, duration: 1 }, '<0.2')
       .to('.step-2', { autoAlpha: 0, y: -40, duration: 1 }, '+=0.5')
       .fromTo('.step-3', { autoAlpha: 0, y: 40 },
                          { autoAlpha: 1, y: 0, duration: 1 }, '<0.2');
     // generalize em loop para N steps
   });
   mm.add('(max-width: 1023.98px)', () => {
     // mobile: sem pin; steps empilhados com reveal simples
     // (y + autoAlpha, ScrollTrigger once por step)
   });
4. Adicione um indicador de progresso vertical (pontos ou barra) que
   acende conforme o progresso — use onUpdate do ScrollTrigger.
5. Na imagem-conceito, um parallax sutil: gsap.to(imagem, { y: -30,
   scrollTrigger: { trigger: wrapper, scrub: true } }).
6. Como o projeto pode usar Lenis, garanta que o ScrollTrigger já esteja
   sincronizado (Prompt 0). Chame ScrollTrigger.refresh() após carregar
   as imagens da seção.

Passo 7 — A joia da coroa: rotação por frame sequence (Motion 13)

Na seção final, um objeto parece girar continuamente ao lado da chamada. O truque tem um século: não é 3D — são N fotografias em ângulos progressivos trocando rapidamente, como um flipbook. O mesmo princípio do desenho animado, o mesmo que a Apple usa nas páginas de produto.

Duas decisões definem o resultado: a quantidade de frames (9 frames em ~2s = estética stop-motion proposital; 24–36 por volta = rotação fluida — decida antes de produzir os assets) e o modo (loop automático ou atrelado ao scroll, o "efeito Apple"). E o detalhe fino do loop: animar até N (não N−1) com módulo no draw — é o que elimina o "soquinho" na emenda da volta.

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

1. N imagens em /public/sequence/frame-1.webp ... frame-N.webp
   (mesmo tamanho, mesma posição do objeto, ângulos progressivos
   cobrindo 360°).
2. Componente com <canvas ref> width/height fixos (ex. 800x800, exibido
   responsivo via CSS w-full max-w-md aspect-square).
3. Em useEffect: pré-carregue as N imagens
   (new Image(), promise em img.onload; Promise.all antes de animar).
   Desenhe o frame 0 assim que carregar.
4. VERSÃO A — rotação automática em loop contínuo:
   const frame = { i: 0 };
   const total = images.length;
   gsap.to(frame, {
     i: total,                       // até N (não N-1) + módulo abaixo:
     duration: 2.2,                  // garante wrap sem "soquinho"
     ease: 'none',                   // no salto do último para o
     repeat: -1,                     // primeiro frame
     snap: 'i',                      // trava em inteiros
     onUpdate: () => {
       ctx.clearRect(0, 0, w, h);
       ctx.drawImage(images[frame.i % total], 0, 0, w, h);
     }
   });
5. VERSÃO B — controlado por scroll (efeito Apple): mesma tween, mas sem
   repeat, i: 0 → total - 1, e com scrollTrigger { trigger: section,
   start: 'top bottom', end: 'bottom top', scrub: true } — o objeto gira
   conforme a rolagem. Implemente as duas atrás de uma prop
   mode: 'auto' | 'scroll'.
6. Só anime quando visível (ScrollTrigger com onToggle pausando/retomando
   a tween, ou IntersectionObserver) para economizar CPU.
7. Textos da seção (h2 + CTA) entram com o padrão de reveal dos motions
   3-4 via ScrollTrigger.
8. Cleanup: kill na tween e nos observers ao desmontar (o useGSAP com
   scope cobre a tween se ela for criada dentro do contexto).

Passo 8 — O fechamento: formulário, idiomas e o menu overlay

Motion 15 — Estados animados do formulário

Ao enviar, o formulário desaparece com fade e no lugar surge a mensagem de sucesso — ou o erro em vermelho com um shake sutil (x: [0, -6, 6, -4, 4, 0]). AnimatePresence mode="wait" orquestra a troca; min-height reservado evita o pulo de layout.

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

1. react-hook-form + zod para validar o e-mail; envio via Server Action
   (app router) com estados: 'idle' | 'loading' | 'success' | 'error'
   em useState.
2. Renderização com <AnimatePresence mode="wait">:
   - status !== 'success': motion.form key="form" com input + botão;
     exit { opacity: 0, y: -10 }.
   - status === 'success': motion.p key="ok" initial { opacity:0, y:10 }
     animate { opacity:1, y:0 } — mensagem de sucesso do projeto.
   - status === 'error': motion.p key="err" com shake sutil:
     animate={{ x: [0, -6, 6, -4, 4, 0] }} transition 0.4s, cor vermelha.
3. Botão em loading: spinner + disabled + leve scale 0.98.
4. Reserve min-height no wrapper para evitar layout shift na troca.
5. Foco/acessibilidade: aria-live="polite" na região das mensagens.

Motion 17 — Seletor de idioma + fade de rota

A InterGest atende empresas alemãs expandindo para o Canadá — o toggle "Deutsch" no header não é decoração, é estratégia. Em Next.js: next-intl com routing por prefixo, sublinhado que se desenha no hover, e o fade global de rota via template.tsx.

txt
Implemente o seletor de idioma em Next.js:

1. Configure next-intl com os locales do projeto (ex.: ['pt', 'en']) e
   routing por prefixo (/en/...). Crie messages/<locale>.json para cada.
2. Componente "LocaleSwitcher": use os helpers de navegação do next-intl
   (createNavigation → { Link, useRouter, usePathname }) para trocar o
   locale preservando a rota atual.
3. Estilo do link: relative, com after: absolute bottom-0 left-0 h-px
   w-full bg-current scale-x-0 origin-left transition-transform
   duration-300 hover:after:scale-x-100 (sublinhado que se desenha).
4. Marque o idioma ativo (aria-current, peso de fonte) e defina o
   atributo lang correto no <html>.
5. A transição de página pode ganhar um fade global: envolva o children
   do layout num template.tsx com Framer Motion (motion.div initial
   { opacity: 0 } animate { opacity: 1 } duration 0.35) — o template.tsx
   remonta a cada navegação, disparando a animação.

Motion 1 — Menu overlay fullscreen com stagger

Fechando o ciclo, o menu: uma cortina escura desce cobrindo a tela (clipPath de cima para baixo), e os links surgem um após o outro subindo de máscaras — variants com staggerChildren, o idioma nativo do Framer Motion para cascatas. Com a ergonomia completa: Escape, focus trap, foco devolvido ao hambúrguer, lenis.stop()/start().

txt
Crie um componente Next.js client "FullscreenMenu" com Framer Motion:

1. Um botão hambúrguer no header controla o estado open (useState). O ícone
   anima entre hambúrguer e X (duas linhas que rotacionam 45°/-45°).
2. Use <AnimatePresence> para montar/desmontar o overlay:
   - Overlay: div fixed inset-0 z-50, fundo escuro; entra com
     clipPath: 'inset(0 0 100% 0)' → 'inset(0 0 0% 0)' (efeito cortina
     de cima para baixo), duration 0.7, ease [0.76, 0, 0.24, 1];
     na saída, reverte.
3. Links de navegação (quantidade parametrizável via array) usam variants:
   - container: staggerChildren: 0.07, delayChildren: 0.3
   - item: hidden { y: '110%', opacity: 0 } → visible { y: 0, opacity: 1 },
     duration 0.6, ease easeOut. Cada link fica dentro de um wrapper com
     overflow-hidden para o efeito de "máscara" (texto surgindo de baixo).
4. Coluna lateral opcional com cards de conteúdo: fade + y:20, delay 0.5.
5. Trava de scroll enquanto aberto:
   - Com Lenis: chame lenis.stop() ao abrir e lenis.start() ao fechar
     (via hook useLenis do Prompt 0). Adicione data-lenis-prevent no
     container do overlay se ele tiver conteúdo rolável interno.
   - Sem Lenis: document.body.style.overflow = 'hidden' com restore no
     cleanup.
6. Feche com tecla Escape e implemente focus trap (foco preso dentro do
   overlay enquanto aberto; ao fechar, devolva o foco ao botão hambúrguer).
7. Respeite prefers-reduced-motion: se ativo, use apenas fade rápido.
Tipografia grande (text-5xl+), links com hover que desloca 8px para a direita.

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

  • "use client" obrigatório em todo componente que toca GSAP, Lenis ou Framer Motion.
  • Sempre useGSAP({ scope }) em vez de useEffect cru — ele faz context.revert() automático no unmount, matando tweens e ScrollTriggers órfãos (a causa nº 1 de bugs ao navegar entre rotas do App Router).
  • Cleanup do Lenis é em 3 passos: gsap.ticker.remove(update) + lenis.off('scroll', ...) + lenis.destroy(). Esquecer o ticker.remove deixa o ticker chamando raf() de instância morta a cada frame.
  • Trava de scroll com Lenis: use lenis.stop()/start(), não apenas overflow: hidden no body; e data-lenis-prevent em overlays com conteúdo rolável.
  • gsap.matchMedia() para responsividade de animações; ScrollTrigger.matchMedia está deprecado desde o GSAP 3.11.
  • ScrollTrigger.refresh() após imagens e fontes carregarem — posições de trigger calculadas antes do layout final ficam erradas.
  • SplitText só depois das fontes: use autoSplit: true + onSplit (GSAP 3.13+) ou envolva em document.fonts.ready.then(). Split antes da fonte = quebras de linha erradas.
  • Flash de conteúdo (FOUC): estados iniciais de elementos animados devem ser setados via gsap.set/autoAlpha ou classe CSS inicial, nunca só no primeiro frame da animação.
  • Pin + Lenis: funciona bem desde que a sincronização do Prompt 0 esteja feita; nunca use smooth scroll de duas bibliotecas ao mesmo tempo.
  • Anime o título do hero uma única vez: se o hero tiver timeline completa (Motion 4), o tween standalone do Motion 3 não deve existir em paralelo.
  • prefers-reduced-motion em tudo — além de acessibilidade, evita enjoo em usuários sensíveis (e é critério de auditoria Lighthouse/axe).

Por onde começar hoje

O trio de entrada: Passo 1 (a infra — o cleanup em três passos do Lenis vale o passo sozinho), Passo 2 (marquee, botões e cookies — três vitórias rápidas que já exercitam a divisão de territórios) e Passo 3 (o hero completo — lembrando da regra de ouro: o título anima uma única vez, dentro da timeline).

E com o quinto capítulo, a série ganha seu contraponto: depois das teses autorais da Plantica, da ANAI, do Studio Modular e da Beetogreen, a InterGest mostra o arranjo clássico bem executado — GSAP coreografando a página, Framer Motion administrando o que nasce e morre, Embla nos trilhos e CSS no que roda sozinho. Não é a arquitetura mais ousada da série; é a que você vai mais usar. Dominar as fronteiras entre as ferramentas é o que impede o codebase de virar briga de bibliotecas — e é exatamente isso que este catálogo treina.

Leia também