{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reactions",
  "title": "Reactions",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Barra de reações em que a contagem rola pro lado que andou e o ícone dá um pulinho quando você reage.",
  "dependencies": [
    "motion",
    "lucide-react",
    "@radix-ui/react-popover"
  ],
  "registryDependencies": [
    "utils",
    "@visant/motion"
  ],
  "files": [
    {
      "path": "registry/visant/ui/reactions.tsx",
      "content": "\"use client\";\n\n/* Adaptado de Reactions — https://uiarc.dev/components/reactions — Copyright (c) 2026 Elia Kuratli, licença MIT (texto integral em ATTRIBUTIONS.md do @visant/ui). */\n\nimport * as React from \"react\";\nimport * as Popover from \"@radix-ui/react-popover\";\nimport {\n  AnimatePresence,\n  LayoutGroup,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  type AnimationPlaybackControls,\n  type TargetAndTransition,\n  type Transition,\n  type Variants,\n} from \"motion/react\";\nimport { Check, SmilePlus, X, type LucideIcon } from \"lucide-react\";\n\nimport { DUR, EASE_OUT, INSTANT } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Reactions — a barra de reações embaixo de comentário, post ou nota de versão.\n *\n * Cada pílula é uma reação e quantas pessoas escolheram. As que são SUAS ganham\n * a marca na borda e no fundo, e o ícone ganha preenchimento: o estado nunca\n * depende só de cor. O botão do fim abre a bandeja com todas as opções, que\n * cresce de dentro dele. Serve pra reconhecimento rápido; quando a pessoa\n * precisa dizer algo específico, o lugar é um campo de resposta.\n *\n * ── O que não é óbvio ──────────────────────────────────────────────────────\n *\n * 1. **Só o dígito que mudou rola.** De 19 pra 20 os dois giram; de 12 pra 13,\n *    só o 3. E rola na direção em que a contagem andou: sobe quando ganha,\n *    desce quando perde. A janela do número acompanha a largura numa mola, então\n *    9 pra 10 não empurra a pílula num quadro.\n * 2. **O ícone leva um impulso, não um keyframe.** Começa parado com\n *    velocidade, passa do ponto e assenta. Dois toques rápidos SOMAM movimento\n *    em vez de recomeçar a animação.\n * 3. **Pílula que zera sai da barra, e o foco não cai na página.** Se ela era o\n *    foco, ele passa pra vizinha, ou pro botão de adicionar.\n * 4. **A barra quebra linha numa mola.** Quando uma pílula nova empurra pra\n *    segunda fileira, a altura segue sem chacoalhar o conteúdo de baixo. Só\n *    anima depois de uma troca de reação: fonte que chega atrasada pula seco.\n * 5. **A bandeja tem uma parada de Tab.** Dentro dela, setas andam e Home/End\n *    vão às pontas; a legenda embaixo diz o nome da opção em foco, pra ícone\n *    nenhum ter que se explicar sozinho.\n *\n * Tudo responde a gesto, então nada passa dos 300 ms. Com\n * `prefers-reduced-motion` as trocas são de opacidade e o número troca seco.\n */\n\nexport interface ReactionOption {\n  id: string;\n  /** Nome falado da reação (\"Curti\", \"Comemorar\"). Vai pro leitor de tela e pra dica. */\n  label: string;\n  icon: LucideIcon;\n}\n\nexport interface Reaction {\n  id: string;\n  count: number;\n  /** Se quem está vendo já reagiu com esta. */\n  reacted: boolean;\n}\n\nexport interface ReactionsProps {\n  /** Todas as reações que a bandeja oferece, na ordem da bandeja. */\n  options: ReactionOption[];\n  /** Controlado. As pílulas seguem esta ordem; contagem zero sai da barra. */\n  value?: Reaction[];\n  defaultValue?: Reaction[];\n  /** Recebe a lista nova e a troca que a produziu. */\n  onValueChange?: (next: Reaction[], change: { id: string; reacted: boolean }) => void;\n  /** Nome da barra pro leitor de tela. */\n  label?: string;\n  className?: string;\n}\n\n// Molas da casa, aparadas no teto de 300 ms de resposta a gesto.\nconst MOLA_TOQUE = { type: \"spring\", visualDuration: 0.26, bounce: 0.12 } as const;\nconst MOLA_FORMA = { type: \"spring\", visualDuration: 0.3, bounce: 0.14 } as const;\nconst MOLA_LISA = { type: \"spring\", visualDuration: 0.3, bounce: 0 } as const;\nconst SAIDA = EASE_OUT;\nconst RAPIDO = DUR.fast;\n\nconst nada = () => () => {};\n/** Movimento reduzido só conta depois de hidratar: servidor e cliente desenham igual. */\nfunction useReduzir() {\n  const hidratado = React.useSyncExternalStore(nada, () => true, () => false);\n  return !!useReducedMotion() && hidratado;\n}\n\n/**\n * Segue o tamanho natural do conteúdo. Depois que `chave` muda, o tamanho vai\n * numa mola; qualquer outro redimensionamento (fonte atrasada, refluxo) pula\n * seco, pra nada balançar no carregamento.\n */\nfunction useSeguirTamanho(\n  conteudo: React.RefObject<HTMLElement | null>,\n  eixo: \"width\" | \"height\",\n  chave: string,\n  reduzir: boolean,\n  mola: Transition,\n) {\n  const tamanho = useMotionValue<number | \"auto\">(\"auto\");\n  const ultima = React.useRef(chave);\n  const armadoAte = React.useRef(0);\n\n  React.useLayoutEffect(() => {\n    if (ultima.current === chave) return;\n    ultima.current = chave;\n    armadoAte.current = performance.now() + 700;\n  }, [chave]);\n\n  React.useEffect(() => {\n    const no = conteudo.current;\n    if (!no || typeof ResizeObserver === \"undefined\") return;\n    let medido = false;\n    let anim: AnimationPlaybackControls | undefined;\n    // borderBoxSize ignora transform: a pílula ainda crescendo informa a largura\n    // real, e o palco escalado da vitrine não encolhe a medida.\n    const obs = new ResizeObserver(([e]) => {\n      if (!e) return;\n      const caixa = e.borderBoxSize?.[0];\n      const prox = caixa\n        ? eixo === \"width\"\n          ? caixa.inlineSize\n          : caixa.blockSize\n        : eixo === \"width\"\n          ? no.offsetWidth\n          : no.offsetHeight;\n      anim?.stop();\n      if (!medido || reduzir || performance.now() > armadoAte.current) {\n        medido = true;\n        tamanho.jump(prox);\n        return;\n      }\n      anim = animate(tamanho, prox, mola);\n    });\n    obs.observe(no, { box: \"border-box\" });\n    return () => {\n      obs.disconnect();\n      anim?.stop();\n    };\n  }, [eixo, conteudo, reduzir, tamanho, mola]);\n\n  return tamanho;\n}\n\nconst borrar = (px: number) => `blur(${px}px)`;\n\nconst digitoVai: Variants = {\n  enter: (dir: number) => ({ opacity: 0, y: `${dir * 0.55}em`, filter: borrar(2) }),\n  center: { opacity: 1, y: \"0em\", filter: borrar(0) },\n  exit: (dir: number) => ({\n    opacity: 0,\n    y: `${dir * -0.55}em`,\n    filter: borrar(2),\n    transition: { duration: RAPIDO, ease: SAIDA },\n  }),\n};\nconst digitoParado: Variants = {\n  enter: { opacity: 1 },\n  center: { opacity: 1 },\n  exit: { opacity: 0, transition: INSTANT },\n};\nconst digitoTransicao = {\n  y: MOLA_TOQUE,\n  opacity: { duration: RAPIDO, ease: SAIDA },\n  filter: { duration: RAPIDO, ease: SAIDA },\n};\n\nfunction Contagem({ value, reduzir }: { value: number; reduzir: boolean }) {\n  const [rolo, setRolo] = React.useState({ value, dir: 1 });\n  // Ajuste durante o render (padrão do React pra estado derivado de prop).\n  if (rolo.value !== value) setRolo({ value, dir: value > rolo.value ? 1 : -1 });\n  const linha = React.useRef<HTMLSpanElement>(null);\n  const texto = String(value);\n  const largura = useSeguirTamanho(linha, \"width\", texto, reduzir, MOLA_FORMA);\n\n  return (\n    <motion.span className=\"inline-flex min-w-0\" style={{ width: largura }} aria-hidden=\"true\">\n      {/* A medida mora neste span interno: filho direto do popLayout não leva ref. */}\n      <span ref={linha} className=\"relative inline-flex w-max\">\n        <AnimatePresence mode=\"popLayout\" initial={false} custom={rolo.dir}>\n          {[...texto].map((d, i) => (\n            <motion.span\n              // A chave conta da direita: a unidade é sempre a mesma casa.\n              key={`${texto.length - i}:${d}`}\n              className=\"inline-block\"\n              custom={rolo.dir}\n              variants={reduzir ? digitoParado : digitoVai}\n              initial=\"enter\"\n              animate=\"center\"\n              exit=\"exit\"\n              transition={reduzir ? INSTANT : digitoTransicao}\n            >\n              {d}\n            </motion.span>\n          ))}\n        </AnimatePresence>\n      </span>\n    </motion.span>\n  );\n}\n\nconst CHUTE = { type: \"spring\", stiffness: 420, damping: 17 } as const;\n\nfunction IconeChute({ icon: Icone, pulso, reduzir }: { icon: LucideIcon; pulso: number; reduzir: boolean }) {\n  const escala = useMotionValue(1);\n  const giro = useMotionValue(0);\n  const visto = React.useRef(0);\n\n  React.useEffect(() => {\n    if (pulso <= visto.current) return;\n    visto.current = pulso;\n    if (reduzir) return;\n    // Soma a velocidade que já existe: toque duplo empurra mais, não recomeça.\n    animate(escala, 1, { ...CHUTE, velocity: escala.getVelocity() + 11 });\n    animate(giro, 0, { ...CHUTE, velocity: giro.getVelocity() - 420 });\n  }, [pulso, reduzir, escala, giro]);\n  React.useEffect(\n    () => () => {\n      escala.stop();\n      giro.stop();\n    },\n    [escala, giro],\n  );\n\n  return (\n    <motion.span className=\"grid size-4 flex-none place-items-center\" style={{ scale: escala, rotate: giro }} aria-hidden=\"true\">\n      <Icone size={16} strokeWidth={1.75} />\n    </motion.span>\n  );\n}\n\nconst repouso: TargetAndTransition = { opacity: 1, scale: 1, y: 0, filter: borrar(0) };\nconst soOpacidade: TargetAndTransition = { opacity: 0 };\nconst someSeco: TargetAndTransition = { opacity: 0, transition: { duration: DUR.press } };\nconst pilulaEntra: TargetAndTransition = { opacity: 0, scale: 0.5, filter: borrar(4) };\nconst pilulaSai: TargetAndTransition = {\n  opacity: 0,\n  scale: 0.6,\n  filter: borrar(2),\n  transition: { duration: RAPIDO, ease: SAIDA },\n};\nconst pilulaTransicao = {\n  layout: MOLA_FORMA,\n  scale: MOLA_TOQUE,\n  opacity: { duration: RAPIDO, ease: SAIDA },\n  filter: { duration: RAPIDO, ease: SAIDA },\n};\nconst iconeEntra: TargetAndTransition = { opacity: 0, scale: 0.6, filter: borrar(2) };\nconst iconeSai: TargetAndTransition = { ...iconeEntra, transition: { duration: RAPIDO, ease: SAIDA } };\nconst iconeTransicao = {\n  ...MOLA_TOQUE,\n  opacity: { duration: RAPIDO, ease: SAIDA },\n  filter: { duration: RAPIDO, ease: SAIDA },\n};\nconst textoEntra: TargetAndTransition = { opacity: 0, y: \"0.3em\", filter: borrar(4) };\nconst textoSai: TargetAndTransition = {\n  opacity: 0,\n  y: \"-0.3em\",\n  filter: borrar(2),\n  transition: { duration: RAPIDO, ease: SAIDA },\n};\n\nconst foco = \"focus-visible:outline-none focus-visible:[box-shadow:var(--focus)]\";\nconst transicaoCor =\n  \"[transition:background-color_var(--dur-fast)_var(--ease-out),border-color_var(--dur-fast)_var(--ease-out),color_var(--dur-fast)_var(--ease-out)]\";\nconst dica =\n  \"pointer-events-none absolute bottom-[calc(100%+8px)] left-1/2 z-10 -translate-x-1/2 translate-y-[3px] whitespace-nowrap rounded-[var(--r-pill)] border border-border bg-card px-2 py-1 text-apoio font-normal leading-tight text-foreground opacity-0 [box-shadow:var(--e-raised)] [transition:opacity_var(--dur-press)_var(--ease-out),translate_var(--dur-fast)_var(--ease-out)]\";\n// A dica entra depois de uma pausa no hover (não pisca ao varrer a barra) e\n// mais rápido no foco de teclado.\nconst dicaAparece =\n  \"group-hover/pilula:translate-y-0 group-hover/pilula:opacity-100 group-hover/pilula:delay-[450ms] group-focus-visible/pilula:translate-y-0 group-focus-visible/pilula:opacity-100 group-focus-visible/pilula:delay-200\";\n\n/** A bandeja: um realce compartilhado desliza até a opção em hover ou foco. */\nfunction Bandeja({\n  options,\n  minhas,\n  reduzir,\n  onPick,\n}: {\n  options: ReactionOption[];\n  minhas: Set<string>;\n  reduzir: boolean;\n  onPick: (id: string) => void;\n}) {\n  const [focoIdx, setFocoIdx] = React.useState(0);\n  const [hover, setHover] = React.useState<number | null>(null);\n  const ativo = Math.min(hover ?? focoIdx, options.length - 1);\n  const botoes = React.useRef<(HTMLButtonElement | null)[]>([]);\n  const x = useMotionValue(0);\n  const posto = React.useRef(false);\n\n  React.useLayoutEffect(() => {\n    // offsetLeft é medida local: não sofre com o palco escalado.\n    const alvo = botoes.current[ativo]?.offsetLeft ?? 0;\n    if (!posto.current || reduzir) {\n      posto.current = true;\n      x.jump(alvo);\n      return;\n    }\n    // Alvo novo redireciona a mola em voo e mantém a velocidade: varrer rápido\n    // desliza em vez de pular de casa em casa.\n    animate(x, alvo, MOLA_FORMA);\n  }, [ativo, reduzir, x]);\n  React.useEffect(() => () => x.stop(), [x]);\n\n  function onKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {\n    const passos: Record<string, number> = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };\n    const n = options.length;\n    const prox =\n      e.key in passos\n        ? (focoIdx + passos[e.key]! + n) % n\n        : e.key === \"Home\"\n          ? 0\n          : e.key === \"End\"\n            ? n - 1\n            : -1;\n    if (prox < 0) return;\n    e.preventDefault();\n    setHover(null);\n    setFocoIdx(prox);\n    botoes.current[prox]?.focus();\n  }\n\n  const opcao = options[ativo];\n  const escolhida = opcao ? minhas.has(opcao.id) : false;\n\n  return (\n    <>\n      <div\n        className=\"relative isolate flex gap-0.5\"\n        role=\"toolbar\"\n        aria-label=\"Reações\"\n        aria-orientation=\"horizontal\"\n        onKeyDown={onKeyDown}\n        onPointerLeave={() => setHover(null)}\n      >\n        <motion.span\n          className=\"absolute left-0 top-0 -z-10 size-10 rounded-[var(--r-pill)] bg-muted\"\n          style={{ x }}\n          aria-hidden=\"true\"\n        />\n        {options.map((item, i) => {\n          const Icone = item.icon;\n          const aceso = i === ativo;\n          const minha = minhas.has(item.id);\n          const atraso = 0.04 + i * 0.035;\n          return (\n            <motion.button\n              key={item.id}\n              ref={(no) => {\n                botoes.current[i] = no;\n              }}\n              type=\"button\"\n              aria-label={item.label}\n              aria-pressed={minha}\n              tabIndex={i === focoIdx ? 0 : -1}\n              className={cn(\n                \"relative grid size-10 pointer-coarse:after:absolute pointer-coarse:after:-inset-0.5 flex-none cursor-pointer place-items-center rounded-[var(--r-pill)] border-0 p-0 [-webkit-tap-highlight-color:transparent]\",\n                transicaoCor,\n                foco,\n                minha\n                  ? \"bg-[color-mix(in_oklab,var(--brand)_16%,transparent)] text-foreground [box-shadow:inset_0_0_0_1px_var(--brand)] [&_svg]:fill-[color-mix(in_oklab,currentColor_22%,transparent)]\"\n                  : \"bg-transparent text-muted-foreground hover:text-foreground\",\n              )}\n              onFocus={() => setFocoIdx(i)}\n              onPointerMove={(e) => {\n                if (e.pointerType === \"mouse\" && hover !== i) setHover(i);\n              }}\n              onClick={() => onPick(item.id)}\n              initial={reduzir ? soOpacidade : iconeEntra}\n              animate={repouso}\n              transition={\n                reduzir\n                  ? { duration: RAPIDO }\n                  : {\n                      ...iconeTransicao,\n                      delay: atraso,\n                      opacity: { ...iconeTransicao.opacity, delay: atraso },\n                      filter: { ...iconeTransicao.filter, delay: atraso },\n                    }\n              }\n            >\n              <motion.span\n                className=\"grid place-items-center\"\n                animate={{ scale: aceso && !reduzir ? 1.14 : 1, y: aceso && !reduzir ? -1 : 0 }}\n                transition={MOLA_TOQUE}\n              >\n                <Icone size={20} strokeWidth={1.75} aria-hidden=\"true\" />\n              </motion.span>\n            </motion.button>\n          );\n        })}\n      </div>\n      <div className=\"relative flex h-[22px] items-center justify-center text-apoio leading-none text-muted-foreground\" aria-hidden=\"true\">\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {opcao && (\n            <motion.span\n              key={`${opcao.id}:${escolhida}`}\n              className=\"inline-flex items-center gap-1 whitespace-nowrap\"\n              initial={reduzir ? soOpacidade : textoEntra}\n              animate={repouso}\n              exit={reduzir ? someSeco : textoSai}\n              transition={{ duration: reduzir ? RAPIDO : 0.24, ease: SAIDA }}\n            >\n              {escolhida && <Check size={12} strokeWidth={2} />}\n              {opcao.label}\n            </motion.span>\n          )}\n        </AnimatePresence>\n      </div>\n    </>\n  );\n}\n\nconst plural = (n: number) => (n === 1 ? \"reação\" : \"reações\");\n\nexport function Reactions({\n  options,\n  value,\n  defaultValue = [],\n  onValueChange,\n  label = \"Reações\",\n  className,\n}: ReactionsProps) {\n  const reduzir = useReduzir();\n  const grupo = React.useId();\n  const [propria, setPropria] = React.useState(defaultValue);\n  const lista = value ?? propria;\n  const [pulsos, setPulsos] = React.useState<Record<string, number>>({});\n  const [aviso, setAviso] = React.useState(\"\");\n  const [aberto, setAberto] = React.useState(false);\n  const barra = React.useRef<HTMLDivElement>(null);\n  const gatilho = React.useRef<HTMLButtonElement>(null);\n  const refocar = React.useRef<string | null>(null);\n\n  const porId = new Map(options.map((o) => [o.id, o]));\n  const visiveis = lista.filter((r) => r.count > 0 && porId.has(r.id));\n  const minhas = new Set(visiveis.filter((r) => r.reacted).map((r) => r.id));\n  const altura = useSeguirTamanho(\n    barra,\n    \"height\",\n    visiveis.map((r) => `${r.id}${r.count}`).join(),\n    reduzir,\n    MOLA_LISA,\n  );\n\n  // Pílula que saiu com o foco entrega o foco à vizinha (ver nota 3).\n  React.useLayoutEffect(() => {\n    const alvo = refocar.current;\n    if (!alvo) return;\n    refocar.current = null;\n    const prox =\n      alvo === \"add\"\n        ? gatilho.current\n        : barra.current?.querySelector<HTMLElement>(`[data-reaction=\"${CSS.escape(alvo)}\"]`);\n    prox?.focus();\n  });\n\n  function alternar(id: string) {\n    const opcao = porId.get(id);\n    if (!opcao) return;\n    const atual = visiveis.find((r) => r.id === id);\n    const somar = !atual?.reacted;\n    const n = Math.max(0, (atual?.count ?? 0) + (somar ? 1 : -1));\n    // O próximo sai da lista desenhada, nunca de dentro de um updater: o\n    // callback do pai roda uma vez só, com o valor certo.\n    const prox = atual\n      ? lista.map((r) => (r.id === id ? { ...r, count: n, reacted: somar } : r)).filter((r) => r.count > 0)\n      : [...lista.filter((r) => r.id !== id), { id, count: 1, reacted: true }];\n\n    if (n === 0) {\n      const pilula = barra.current?.querySelector(`[data-reaction=\"${CSS.escape(id)}\"]`);\n      if (pilula && pilula === document.activeElement) {\n        const i = visiveis.findIndex((r) => r.id === id);\n        refocar.current = visiveis[i + 1]?.id ?? visiveis[i - 1]?.id ?? \"add\";\n      }\n    }\n    if (somar) setPulsos((p) => ({ ...p, [id]: (p[id] ?? 0) + 1 }));\n    if (value === undefined) setPropria(prox);\n    setAviso(\n      somar\n        ? `Você reagiu com ${opcao.label}. ${n} ${plural(n)}.`\n        : n\n          ? `Você tirou ${opcao.label}. ${n} ${plural(n)}.`\n          : `Você tirou ${opcao.label}.`,\n    );\n    onValueChange?.(prox, { id, reacted: somar });\n  }\n\n  function escolher(id: string) {\n    setAberto(false);\n    gatilho.current?.focus();\n    alternar(id);\n  }\n\n  return (\n    <motion.div className={cn(\"relative min-w-0\", className)} style={{ height: altura }}>\n      <div ref={barra} className=\"relative flex flex-wrap items-center gap-x-1.5 gap-y-2 pointer-coarse:gap-y-3\" role=\"group\" aria-label={label}>\n        <LayoutGroup id={grupo}>\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            {visiveis.map((r) => {\n              const opcao = porId.get(r.id)!;\n              return (\n                <motion.button\n                  key={r.id}\n                  layout=\"position\"\n                  type=\"button\"\n                  data-reaction={r.id}\n                  aria-pressed={r.reacted}\n                  aria-label={`${opcao.label}, ${r.count} ${plural(r.count)}${r.reacted ? \", incluindo a sua\" : \"\"}`}\n                  onClick={() => alternar(r.id)}\n                  className={cn(\n                    \"group/pilula relative inline-flex h-8 pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:-inset-y-2 flex-none cursor-pointer items-center gap-1.5 rounded-[var(--r-pill)] border py-0 pl-2.5 pr-3 text-corpo font-medium leading-none tabular-nums [-webkit-tap-highlight-color:transparent] [touch-action:manipulation] hover:z-10 focus-visible:z-10\",\n                    transicaoCor,\n                    foco,\n                    r.reacted\n                      ? \"border-[var(--brand)] bg-[color-mix(in_oklab,var(--brand)_14%,var(--card))] text-foreground hover:bg-[color-mix(in_oklab,var(--brand)_22%,var(--card))] [&_svg]:fill-[color-mix(in_oklab,currentColor_22%,transparent)]\"\n                      : \"border-border bg-card text-muted-foreground hover:bg-muted hover:text-foreground\",\n                  )}\n                  initial={reduzir ? soOpacidade : pilulaEntra}\n                  animate={repouso}\n                  exit={reduzir ? someSeco : pilulaSai}\n                  whileTap={{ scale: reduzir ? 1 : 0.97, transition: { duration: RAPIDO, ease: SAIDA } }}\n                  transition={reduzir ? { duration: RAPIDO, layout: INSTANT } : pilulaTransicao}\n                >\n                  <IconeChute icon={opcao.icon} pulso={pulsos[r.id] ?? 0} reduzir={reduzir} />\n                  <Contagem value={r.count} reduzir={reduzir} />\n                  <span className={cn(dica, dicaAparece)} aria-hidden=\"true\">\n                    {opcao.label}\n                  </span>\n                </motion.button>\n              );\n            })}\n          </AnimatePresence>\n\n          <Popover.Root open={aberto} onOpenChange={setAberto}>\n            {/* O botão é a âncora da bandeja: responde ao toque com cor, nunca com escala. */}\n            <Popover.Trigger asChild>\n              <motion.button\n                ref={gatilho}\n                layout=\"position\"\n                transition={reduzir ? { layout: INSTANT } : { layout: MOLA_FORMA }}\n                type=\"button\"\n                aria-label=\"Adicionar reação\"\n                className={cn(\n                  \"group/pilula relative inline-flex h-8 w-[38px] pointer-coarse:after:absolute pointer-coarse:after:-inset-x-1 pointer-coarse:after:-inset-y-2 flex-none cursor-pointer items-center justify-center rounded-[var(--r-pill)] border border-border bg-card p-0 text-muted-foreground [-webkit-tap-highlight-color:transparent] hover:z-10 hover:bg-muted hover:text-foreground focus-visible:z-10 data-[state=open]:bg-muted data-[state=open]:text-foreground\",\n                  transicaoCor,\n                  foco,\n                )}\n              >\n                <span className=\"grid size-4 place-items-center\" aria-hidden=\"true\">\n                  <AnimatePresence initial={false}>\n                    <motion.span\n                      key={aberto ? \"fechar\" : \"abrir\"}\n                      className=\"[grid-area:1/1] grid place-items-center\"\n                      initial={reduzir ? soOpacidade : iconeEntra}\n                      animate={repouso}\n                      exit={reduzir ? someSeco : iconeSai}\n                      transition={reduzir ? { duration: RAPIDO } : iconeTransicao}\n                    >\n                      {aberto ? <X size={16} strokeWidth={1.75} /> : <SmilePlus size={16} strokeWidth={1.75} />}\n                    </motion.span>\n                  </AnimatePresence>\n                </span>\n                {!aberto && (\n                  <span className={cn(dica, dicaAparece)} aria-hidden=\"true\">\n                    Adicionar reação\n                  </span>\n                )}\n              </motion.button>\n            </Popover.Trigger>\n            <AnimatePresence>\n              {aberto && (\n                <Popover.Portal forceMount>\n                  {/* O foco volta à mão: na hora depois de escolher ou Esc, e nunca quando se clica fora. */}\n                  <Popover.Content\n                    forceMount\n                    asChild\n                    side=\"top\"\n                    align=\"center\"\n                    sideOffset={8}\n                    collisionPadding={12}\n                    aria-label=\"Escolher reação\"\n                    onEscapeKeyDown={() => gatilho.current?.focus()}\n                    onCloseAutoFocus={(e) => e.preventDefault()}\n                  >\n                    <motion.div\n                      className=\"z-[60] grid origin-[var(--radix-popover-content-transform-origin)] gap-0.5 rounded-[var(--r-surface)] border border-border bg-card p-1 text-foreground outline-none [box-shadow:var(--e-raised)] data-[state=closed]:pointer-events-none\"\n                      initial={reduzir ? soOpacidade : { opacity: 0, scale: 0.5, filter: borrar(4) }}\n                      animate={repouso}\n                      exit={\n                        reduzir\n                          ? someSeco\n                          : { opacity: 0, scale: 0.9, filter: borrar(2), transition: { duration: RAPIDO, ease: SAIDA } }\n                      }\n                      transition={\n                        reduzir\n                          ? { duration: RAPIDO }\n                          : {\n                              scale: MOLA_TOQUE,\n                              opacity: { duration: RAPIDO, ease: SAIDA },\n                              filter: { duration: RAPIDO, ease: SAIDA },\n                            }\n                      }\n                    >\n                      {/* Seta invisível: com ela o Radix põe a origem da escala no centro\n                          do botão, e a bandeja nasce dele mesmo quando desvia da borda. */}\n                      <Popover.Arrow width={0} height={0} className=\"invisible\" />\n                      <Bandeja options={options} minhas={minhas} reduzir={reduzir} onPick={escolher} />\n                    </motion.div>\n                  </Popover.Content>\n                </Popover.Portal>\n              )}\n            </AnimatePresence>\n          </Popover.Root>\n        </LayoutGroup>\n      </div>\n      <span className=\"sr-only\" role=\"status\">\n        {aviso}\n      </span>\n    </motion.div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Barra de reações em post, comentário ou mensagem, com a contagem rolando",
      "evite": "Avaliação formal de produto: estrelas ou nota; quem está numa tarefa, assignee-stack"
    }
  },
  "categories": [
    "controles",
    "reacao",
    "comentario",
    "contador",
    "emoji",
    "feedback"
  ],
  "type": "registry:ui"
}