O que Reação Animação faz?
A versão instalada não possui docs/react/ — se o caminho docs/... deste arquivo não puder ser aberto, tente ler adicionando o mesmo caminho após https://raw.githubusercontent.com/joo6077/claude-plugins/main/. Se ainda assim não for possível, não invente conteúdo; apenas registre que não foi possível ler.
Pegadinhas
-
Princípio de zero bibliotecas — sem exceções: Motion (framer-motion) / dnd-kit / react-spring / react-transition-group / @formkit/auto-animate / react-dnd / gsap / lottie-react / react-beautiful-dnd / animate.css são proibidos de instalar e importar. Se alguma importação dessas existir na base de código,
/react-auditemitirá uma falha de build. Mesmo que o usuário solicite, ofereça uma implementação alternativa e recuse o uso de bibliotecas. -
Comece sempre pelo nível mais baixo: Ao receber uma solicitação de "animação", é comum pular para o Tier 3. Siga as regras de decisão: verifique primeiro se o Tier 1 resolve. Só avance para tiers superiores quando for estritamente necessário.
-
Verifique o plugin
tailwindcss-animate: Utilitários comoanimate-in,fade-in-*,slide-in-from-*exigem otailwindcss-animate. Projetos com shadcn já o incluem por padrão, mas, se estiver ausente, executepnpm add -D tailwindcss-animatee registre o plugin notailwind.config.ts. -
Unicidade do
view-transition-name: Dois elementos não podem ter o mesmo nome simultaneamente, senão ocorre o erro "duplicate name". Use IDs dinâmicos (ex.:view-transition-name: task-${item.id}) para garantir a unicidade. -
Tratamento obrigatório de
pointercancel: Ignorar o eventopointercancelpode causar vazamento de estado de arrasto durante troca de abas, foco em outro aplicativo ou desmontagem de elemento. No manipuladoronPointerCancel, redefina a FSM paraidle. -
touch-action: noneé essencial para arrasto em dispositivos móveis: Em elementos com arrasto baseado em pointer events, adicione a classe Tailwindtouch-nonepara bloquear rolagem e zoom padrão do navegador. -
Store de arrasto do Zustand é uma instância global única: Não crie stores de arrasto separadas por funcionalidade. Use
src/presentation/shared/stores/drag-store.tscomo única store compartilhada por todas as funcionalidades. -
prefers-reduced-motiondeve ser respeitado: Em todos os tiers, desabilite animações com@media (prefers-reduced-motion: reduce)ou com a variantemotion-reduce:*do Tailwind. O/react-auditemitirá um aviso se estiver ausente. -
Animações são exclusivas da camada de apresentação: As camadas
domain/edata/não podem importar pointer events, view transitions ou a store de arrasto do Zustand. Código de animação deve estar apenas emsrc/presentation/. -
Firefox não suporta o parâmetro
types: A opçãotypesemdocument.startViewTransition({ update, types: [...] })é suportada de forma estável apenas em navegadores Chromium (a partir de 2026). Use try/catch no wrapperwithViewTransitionpara fallback. -
Suporte a scroll-driven animation (atualizado em 2026-08-13):
animation-timeline: scroll()/scroll-timelinetem 85,43% de cobertura global (Can I Use) — Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+. A afirmação de que o Firefox está atrás de uma flag está desatualizada. No entanto,view()ainda não é Baseline segundo o MDN. Portanto, mantenha a política de usar@supports (animation-timeline: scroll())/@supports (animation-timeline: view())com fallback para estilos estáticos. -
Uso de
view-transition-name: match-element(Chrome 137+): Em vez de atribuir nomes únicos manualmente a dezenas de elementos, use o valor CSSview-transition-name: match-elementpara que o navegador faça a correspondência automaticamente. No entanto, é exclusivo do Chromium (2026-Q2), então mantenha uma rota de fallback. -
Movimento não é comprovado por código estático — evidência obrigatória antes de declarar conclusão (E2): O resultado deste skill só pode ser verificado quando reproduzido. O fato de classes
animate-*estarem presentes ou de o código chamarstartViewTransitioné evidência estática (R3), não prova que a animação realmente foi executada — keyframes indefinidos,prefers-reduced-motionsempre ativo, clipping poroverflow: hiddenno pai, ou entrada no fallback de navegadores não suportados podem silenciosamente resultar em "nada acontece". Antes de finalizar, preencha a checklist da seção §4 dereact-kit/references/render-evidence-protocol.md. Ao verificar com comparação de snapshots, aponte dois momentos (antes/depois) — um único frame estático não é um oráculo para qualquer movimento (§3 d). Se não for possível obter evidência, adicione[Não Verificado]e preencha os quatro campos (o que impede, tentativas de contorno, motivo fora de controle, comando para re-verificação — conforme §2 do protocolo) e reporte como conclusão parcial.Não introduza bibliotecas de animação para obter evidências. A Política de Bibliotecas (Pegadinha #1) está acima deste protocolo e não é flexibilizada por nenhuma conveniência de verificação.
-
Capture a referência antes de editar — Execute o Passo 0 da §1 e o primeiro item da sequência de comparação da §2 de
react-kit/references/render-evidence-protocol.mdantes da primeira edição. Registre o resultado (paráfrase, tabela de convenções, caminho do instantâneo do estado atual e marcações do que precisa mudar) na resposta. Depois de editar, não é possível capturar o estado anterior novamente. -
Tier 2 é dividido pelo tipo de atualização —
<ViewTransition>do React 19.3 é API estável (adicionado em 2026-09-26): O<ViewTransition>importado dereacttornou-se uma API estável no React 19.3, não mais experimental. Ele é executado em atualizações marcadas como Transition (atualizações dentro destartTransition, Suspense reveal,useDeferredValue) quando o estilo muda ou ocorre montagem/desmontagem, e funciona apenas no DOM (fonte: https://react.dev/blog/2026/09/09/react-19-3 «We shared it as an experimental API last year, and in 19.3 it's stable and ready to use.»). Portanto, para atualizações de estado React marcadas como Transition, use<ViewTransition>; para outras atualizações DOM (atualizações síncronas não-Transition, manipulação DOM fora do React), mantenha o wrapperwithViewTransitionda §3.1. Não remova o wrapper. Como é um componente do pacotereact, não viola o princípio de zero bibliotecas (#1). Se a versão do React no projeto for inferior a 19.3, use apenas o wrapper.
Processo
1. Decisão Automática de Nível
Se a flag --tier não estiver presente, analise a solicitação do usuário e escolha o nível mais baixo.
| Palavra-chave / Cenário | Nível Decidido |
|---|---|
| "fade in", "slide up", "scale", "pulse", "bounce", "efeito hover", "opacity", "shimmer", "animação de entrada" | T1 |
| "transição de carregamento de skeleton → conclusão", "hover em botão", "accordion", "modal abrir/fechar", "mudança de estado" | T1 |
| "animação de scroll", "scroll-driven", "parallax", "barra de progresso de scroll", "baseado em scroll" | T1 |
| "transição de página", "elemento compartilhado", "grid para board", "transição de visualização", "transição de rota", "mudança de estrutura DOM" | T2 |
| "scroll-triggered", "disparo ao cruzar scroll", "iniciar animação em posição específica" | T2 |
| "arrastar e soltar", "lista ordenável", "sortable", "kanban", "arrastar", "soltar", "gesto", "linhas de conexão com setas" | T3 |
Se o limite entre T2/T3 estiver ambíguo, consulte o agente animation-architect-react.
Resumo dos 3 Níveis:
| Nível | Ferramenta | Cenário de Aplicação | Dificuldade |
|---|---|---|---|
| T1 | Utilitários Tailwind animate-* + CSS @keyframes + scroll-driven | Mudança de estado, hover, loop simples, scroll | Baixa |
| T2 | View Transitions API (wrapper withViewTransition) · React 19.3 <ViewTransition> (apenas atualizações marcadas como Transition — Pegadinha 15) | Transição de visualização/rota, elemento compartilhado, FLIP | Média |
| T3 | Pointer Events + FSM + requestAnimationFrame | Arrastar e soltar, gestos, linhas de conexão SVG | Alta |
2. Nível 1 — Implementação com Tailwind + CSS
2.1 Utilitários Internos do Tailwind
| Categoria | Utilitário | Exemplo |
|---|---|---|
| Transição | transition-transform, transition-colors, transition-opacity | hover:scale-105 transition-transform duration-200 |
| Duração | duration-75~duration-1000 | duration-300 |
| Easing | ease-linear, ease-in, ease-out, ease-in-out | ease-in-out |
| Animação (interna) | animate-spin, animate-ping, animate-pulse, animate-bounce | Carregamento, notificação |
| Atraso | delay-75~delay-1000 | Efeito de stagger |
2.2 Utilitários Estendidos do tailwindcss-animate
animações de entrada/saída fornecidas pelo plugin tailwindcss-animate:
animate-in/animate-outfade-in-*,fade-out-*slide-in-from-top/bottom/left/right-*,slide-out-to-*zoom-in-*,zoom-out-*
Combine com o seletor de atributo data-[state=...] para implementar animações baseadas em estado:
// Exemplo de drawer abrir/fechar
<div
data-state={open ? 'open' : 'closed'}
className="
data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left
duration-300
motion-reduce:transition-none motion-reduce:animate-none
"
>
Conteúdo do Drawer
</div>
2.3 Declaração Direta de CSS @keyframes
Para animações personalizadas não cobertas pelos utilitários do Tailwind, declare no bloco @theme do globals.css:
/* src/presentation/styles/globals.css */
@import "tailwindcss";
@theme {
--animate-wiggle: wiggle 1s ease-in-out infinite;
--animate-shimmer: shimmer 2s linear infinite;
@keyframes wiggle {
0%, 100% { transform: rotate(-3deg); }
50% { transform: rotate(3deg); }
}
@keyframes shimmer {
from { background-position: -200% 0; }
to { background-position: 200% 0; }
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Uso: <div className="animate-wiggle">...</div>
2.4 Exemplos de Cenários do Nível 1
Entrada com fade-in:
<div className="animate-in fade-in-0 duration-500 motion-reduce:animate-none">
Entrada com fade ao carregar a página
</div>
Botão com hover scale:
<button className="transition-transform duration-200 hover:scale-105 active:scale-95 motion-reduce:transition-none">
Botão
</button>
Entrada em lista com stagger:
{items.map((item, i) => (
<div
key={item.id}
className="animate-in slide-in-from-bottom-4 fade-in-0 motion-reduce:animate-none"
style={{ animationDelay: `${i * 50}ms`, animationFillMode: 'backwards' }}
>
{item.name}
</div>
))}
2.5 Animações Scroll-driven (CSS nativo)
Implemente animações vinculadas à posição de scroll usando CSS animation-timeline, sem JavaScript. Em 2026-08-13, scroll() / scroll-timeline é suportado no Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+ (85,43% global segundo Can I Use), enquanto view() ainda não é Baseline pelo MDN. Em ambos os casos, envolva com @supports para fallback gracioso em navegadores não suportados.
Barra de progresso de scroll:
/* globals.css */
@keyframes progress-grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
<div
className="fixed top-0 left-0 h-1 w-full origin-left bg-primary motion-reduce:hidden"
style={{
animation: 'progress-grow linear',
animationTimeline: 'scroll(root block)',
}}
/>
Fade-in ao entrar na tela (view timeline):
@keyframes fade-slide-in {
from { opacity: 0; transform: translateY(2rem); }
to { opacity: 1; transform: translateY(0); }
}
@supports (animation-timeline: view()) {
.scroll-reveal {
animation: fade-slide-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}
<section className="scroll-reveal motion-reduce:opacity-100">
Conteúdo que aparece ao scrollar
</section>
Atenção: Animações scroll-triggered (Chrome 145, 2026) são um conceito diferente que dispara animações baseadas no tempo ao cruzar um deslocamento de scroll específico. Não confunda com scroll-driven. Scroll-triggered é classificado como candidato ao Nível 2.
3. Nível 2 — Implementação com View Transitions API
View Transitions no mesmo documento é Baseline 2025 pelo MDN, com 90,2% de cobertura global pelo Can I Use — Chrome/Edge 111+, Safari 18+, Firefox 144+ (atualizado em 2026-08-13). Fornece animação FLIP automática para mudanças no DOM de um SPA.
Por outro lado, a transição cross-document / MPA baseada em @view-transition e alguns recursos detalhados ainda têm disponibilidade limitada. Ao usar para transições de rota, mantenha o caminho same-document como padrão com feature detection e fallback (§6, lacuna de cobertura padrão nº 8).
3.1 Criação do Wrapper withViewTransition
Crie o arquivo se não existir:
// src/presentation/shared/lib/view-transition.ts
export function withViewTransition(
updateDOM: () => void,
options?: { types?: string[] },
): void {
// Respeita prefers-reduced-motion
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
updateDOM()
return
}
// Feature detection (fallback para navegadores antigos)
if (typeof document.startViewTransition !== 'function') {
updateDOM()
return
}
// Tratamento de suporte ao parâmetro types (Firefox não suporta na implementação inicial)
try {
if (options?.types && options.types.length > 0) {
document.startViewTransition({ update: updateDOM, types: options.types })
} else {
document.startViewTransition(updateDOM)
}
} catch {
updateDOM()
}
}
3.2 Elementos Compartilhados com view-transition-name no CSS
Mesmo que a estrutura DOM mude, identifique como "mesmo elemento" para uma transição suave:
// Garanta unicidade com ID dinâmico (nomes duplicados são proibidos)
<div
className="rounded-lg border p-4"
style={{ viewTransitionName: `task-${task.id}` }}
>
{task.title}
</div>
3.3 Transição de Visualização Grid ↔ Board (Cenário Principal)
// src/presentation/features/tasks/components/task-switcher.tsx
import { withViewTransition } from '@/presentation/shared/lib/view-transition'
import { useTaskStore } from '../store'
export function TaskViewSwitcher() {
const viewMode = useTaskStore((s) => s.viewMode)
const setViewMode = useTaskStore((s) => s.setViewMode)
return (
<button
onClick={() => {
withViewTransition(() => {
setViewMode(viewMode === 'grid' ? 'board' : 'grid')
})
}}
>
{viewMode === 'grid' ? 'Para visualização em board' : 'Para visualização em grid'}
</button>
)
}
export function TaskList({ tasks }: { tasks: Task[] }) {
const viewMode = useTaskStore((s) => s.viewMode)
return (
<div className={viewMode === 'grid' ? 'grid grid-cols-3 gap-4' : 'flex gap-4 overflow-x-auto'}>
{tasks.map((task) => (
<div
key={task.id}
className="rounded-lg border p-4"
style={{ viewTransitionName: `task-${task.id}` }}
>
{task.title}
</div>
))}
</div>
)
}
3.4 Ajustes Finos na Transição CSS
/* globals.css — duração da transição + easing */
::view-transition-group(*) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Ajuste individual para elementos específicos */
::view-transition-group(hero-image) {
animation-duration: 0.6s;
}
/* Transição instantânea com reduced-motion */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*) {
animation-duration: 0.01ms;
}
}
4. Nível 3 — Implementação de Primitivas de Pointer Personalizadas
Implemente diretamente com Pointer Events API + máquina de estados finita (FSM), sem bibliotecas.
4.1 Criação do Hook useDrag
// src/presentation/shared/hooks/use-drag.ts
import { useCallback, useEffect, useReducer, useRef } from 'react'
// FSM 3 estados: idle / dragging / dropping
type DragState =
| { kind: 'idle' }
| { kind: 'dragging'; startX: number; startY: number; offsetX: number; offsetY: number; pointerId: number }
| { kind: 'dropping'; offsetX: number; offsetY: number }
type DragAction =
| { type: 'start'; x: number; y: number; pointerId: number }
| { type: 'move'; x: number; y: number }
| { type: 'drop' }
| { type: 'resolved' }
| { type: 'cancel' }
function dragReducer(state: DragState, action: DragAction): DragState {
switch (action.type) {
case 'start':
return { kind: 'dragging', startX: action.x, startY: action.y, offsetX: 0, offsetY: 0, pointerId: action.pointerId }
case 'move':
if (state.kind !== 'dragging') return state
return { ...state, offsetX: action.x - state.startX, offsetY: action.y - state.startY }
case 'drop':
if (state.kind !== 'dragging') return state
return { kind: 'dropping', offsetX: state.offsetX, offsetY: state.offsetY }
case 'resolved':
case 'cancel':
return { kind: 'idle' }
}
}
type DragHandlers = {
onPointerDown: (e: React.PointerEvent) => void
onPointerMove: (e: React.PointerEvent) => void
onPointerUp: (e: React.PointerEvent) => void
onPointerCancel: (e: React.PointerEvent) => void
}
export function useDrag(): {
state: DragState
handlers: DragHandlers
ref: React.RefObject<HTMLElement | null>
resolve: () => void
} {
const [state, dispatch] = useReducer(dragR
...
}
4.2 Uso do useDrag
// src/presentation/features/tasks/components/draggable-task-card.tsx
import { useCallback, useEffect } from 'react'
import { useDrag } from '@/presentation/shared/hooks/use-drag'
import { useDragStore } from '@/presentation/shared/stores/drag-store'
export function DraggableTaskCard({ task }: { task: Task }) {
const { state, handlers, ref, resolve } = useDrag()
const { setDragState } = useDragStore()
useEffect(() => {
if (state.kind === 'dragging' || state.kind === 'dropping') {
setDragState(state)
}
}, [state, setDragState])
return (
<div
ref={ref}
className="touch-none select-none rounded-lg border p-4 transition-shadow"
style={{
transform: state.kind === 'dragging'
? `translate(${state.offsetX}px, ${state.offsetY}px)`
: 'translate(0, 0)',
opacity: state.kind === 'dragging' ? 0.8 : 1,
zIndex: state.kind === 'dragging' ? 50 : 'auto',
}}
{...handlers}
>
{task.title}
</div>
)
}
4.3 Gerenciamento de Arrasto com Drop Zone (Exemplo de Lista Ordenável)
Use a store global de arrasto para coordenar a zona de soltar:
// src/presentation/features/tasks/components/task-list.tsx
import { useDragStore } from '@/presentation/shared/stores/drag-store'
import { useTaskStore } from '../store'
export function TaskList({ tasks }: { tasks: Task[] }) {
const setTaskPosition = useTaskStore((s) => s.setTaskPosition)
const dragState = useDragStore((s) => s.state)
const handleDrop = useCallback(
(targetId: string) => {
if (dragState.kind === 'dropping') {
setTaskPosition(dragState.itemId, targetId)
useDragStore.getState().resolve()
}
},
[dragState, setTaskPosition],
)
return (
<div className="space-y-2">
{tasks.map((task) => (
<div
key={task.id}
className={`rounded-lg border p-4 ${
dragState.kind === 'dragging' && dragState.itemId !== task.id
? 'border-dashed border-blue-500'
: ''
}`}
onPointerUp={() => handleDrop(task.id)}
>
<DraggableTaskCard task={task} />
</div>
))}
</div>
)
}
5. Verificação de Evidências (Obrigatório)
Consulte react-kit/references/render-evidence-protocol.md para obter o protocolo completo de verificação. Os pontos principais:
- Evidência R3 (Código Estático): A existência de código de animação ainda não prova que a animação funciona. Necessário evidência reproduzível.
- Evidência R4 (Reproduzível): Capture capturas de tela (antes/depois) ou grave a animação.
- Fallback: Se não for possível verificar, registre como
[Não Verificado]com os quatro campos obrigatórios.
6. Padrões e Convenções
- Nomes de arquivos: Use kebab-case para hooks e componentes de animação (ex.:
use-drag.ts,view-transition.ts). - Estrutura de pastas: Código de animação em
src/presentation/shared/(hooks, lib, stores). - Store global: Use
src/presentation/shared/stores/drag-store.tsapenas. - Acessibilidade: Sempre aplique
prefers-reduced-motion. - Suporte a navegadores: Use fallbacks para navegadores não suportados (especialmente Firefox para View Transitions com
types).
7. Perguntas Frequentes
P: Posso usar Framer Motion se o usuário insistir? R: Não. A política de zero bibliotecas é absoluta. Ofereça uma implementação alternativa com os Tiers descritos.
P: Quando usar <ViewTransition> do React 19.3?
R: Apenas para atualizações de estado React marcadas como Transition (ex.: dentro de startTransition). Para outras atualizações DOM, use withViewTransition.
P: Como lidar com animações em scroll-triggered?
R: Classifique como Tier 2 e use IntersectionObserver com CSS ou View Transitions.
P: E se eu precisar de gestos complexos?
R: Implemente manualmente com Pointer Events + FSM (Tier 3). Consulte o exemplo useDrag.
P: Como garantir que a animação funciona em dispositivos móveis?
R: Adicione touch-action: none (ou classe touch-none do Tailwind) e trate pointercancel.
P: O que fazer se o view-transition-name duplicar?
R: Use IDs dinâmicos (ex.: task-${item.id}) para garantir unicidade.
P: Como desabilitar animações para usuários com prefers-reduced-motion?
R: Use @media (prefers-reduced-motion: reduce) ou a variante motion-reduce:* do Tailwind em todos os Tiers.