{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "icon-morph",
  "title": "Icon Morph",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Ícone de dois estados que vira o outro em vez de trocar, como menu e fechar ou copiar e copiado.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "@visant/motion"
  ],
  "files": [
    {
      "path": "registry/visant/ui/icon-morph.tsx",
      "content": "\"use client\";\n\n// Adaptado de Icon morph — https://uiarc.dev/components/icon-morph — Copyright (c) 2026 Elia Kuratli, licença MIT (texto integral em ATTRIBUTIONS.md do @visant/ui).\n\nimport * as React from \"react\";\nimport { motion, useReducedMotion, type Transition } from \"motion/react\";\n\nimport { DUR, INSTANT } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Icon Morph: o ícone que muda de estado em vez de ser trocado.\n *\n * Botão de ícone que alterna (menu e fechar, tocar e pausar, copiar e\n * copiado) costuma resolver com dois `<svg>` e um ternário. O olho lê isso\n * como pisca: some uma coisa, aparece outra, e a relação entre as duas fica\n * por conta de quem olha. Aqui as linhas de um estado VIRAM as linhas do\n * outro, e a mudança conta o que aconteceu.\n *\n * ── Decisões que não são óbvias ────────────────────────────────────────────\n *\n * 1. **Os traços são desenhados aqui, e é a exceção consciente à regra da\n *    casa.** Morph precisa saber qual linha vira qual, e um ícone pronto do\n *    lucide é uma caixa fechada. A grade é a mesma do lucide (24 unidades,\n *    traço 1,75, ponta redonda), então ao lado de um ícone lucide não destoa.\n * 2. **Mola interrompível em tudo.** Quem clica duas vezes rápido vê o ícone\n *    voltar de onde estava, não do começo. Por isso nada aqui usa duração fixa\n *    no caminho principal.\n * 3. **O ícone é mudo.** Sai com `aria-hidden`; quem fala é o botão em volta,\n *    com `aria-pressed`, `aria-expanded` ou um rótulo que muda. Ícone que se\n *    anuncia sozinho vira leitura dupla.\n * 4. **Movimento reduzido chega no estado final sem viagem.** O estado é\n *    informação; a viagem é enfeite.\n *\n * A cor vem de `currentColor`: pinte pelo botão, nunca pelo ícone.\n */\n\nexport type IconMorphName = \"menu\" | \"play\" | \"plus\" | \"copy\" | \"theme\" | \"chevron\";\n\nexport interface IconMorphProps\n  extends Omit<React.SVGProps<SVGSVGElement>, \"ref\" | \"children\" | \"name\"> {\n  /** Qual par de estados desenhar. */\n  name: IconMorphName;\n  /** O segundo estado: fechar, pausar, check, lua, seta virada. */\n  state?: boolean;\n  /** Tamanho em px. */\n  size?: number;\n  /** Espessura do traço na grade de 24. */\n  strokeWidth?: number;\n}\n\n// As molas da casa, em `visualDuration`. Ícone troca dezenas de vezes por\n// sessão: a mola fica bem abaixo do teto de 300 ms, rápida o bastante pra\n// nunca parecer que o clique esperou a animação.\nconst MOLA: Transition = { type: \"spring\", visualDuration: 0.18, bounce: 0.15 };\nconst MOLA_FIRME: Transition = { type: \"spring\", visualDuration: 0.14, bounce: 0.05 };\nconst MOLA_LISA: Transition = { type: \"spring\", visualDuration: 0.16, bounce: 0 };\nconst JA: Transition = INSTANT;\nconst SUMIR = { duration: DUR.press };\n// O traço acende quase de uma vez: a mola desenha, a opacidade só evita a ponta solta.\nconst ACENDE_S = 0.06;\n\nfunction Moldura({\n  size = 24,\n  strokeWidth = 1.75,\n  className,\n  children,\n  ...rest\n}: Omit<IconMorphProps, \"name\" | \"state\"> & { children: React.ReactNode }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width={size}\n      height={size}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={strokeWidth}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      className={cn(\"block flex-none overflow-visible\", className)}\n      {...rest}\n    >\n      {children}\n    </svg>\n  );\n}\n\n// O triângulo é cortado ao meio em dois quadriláteros: cada metade vira uma barra da pausa.\nconst PLAY = [\"M7 4.5 L12 7.42 L12 16.58 L7 19.5 Z\", \"M12 7.42 L19.5 11.8 L19.5 12.2 L12 16.58 Z\"];\nconst PAUSA = [\"M6.5 5 L10 5 L10 19 L6.5 19 Z\", \"M14 5 L17.5 5 L17.5 19 L14 19 Z\"];\n\nconst RAIOS = Array.from({ length: 8 }, (_, i) => {\n  const a = (i * Math.PI) / 4;\n  const r = (v: number) => Math.round(v * 100) / 100;\n  return `M${r(12 + Math.cos(a) * 7.5)} ${r(12 + Math.sin(a) * 7.5)} L${r(12 + Math.cos(a) * 10)} ${r(12 + Math.sin(a) * 10)}`;\n});\n\nexport function IconMorph({ name, state = false, ...props }: IconMorphProps) {\n  const reduzido = useReducedMotion() ?? false;\n  const id = React.useId().replace(/[^a-zA-Z0-9-]/g, \"\");\n  const t = reduzido ? JA : MOLA;\n\n  switch (name) {\n    // As linhas de fora descem ao meio girando até o X; a do meio se recolhe.\n    case \"menu\":\n      return (\n        <Moldura {...props}>\n          <motion.path d=\"M4 6h16\" initial={false} animate={state ? { y: 6, rotate: 45 } : { y: 0, rotate: 0 }} transition={t} />\n          <motion.path\n            d=\"M4 12h16\"\n            initial={false}\n            animate={state ? { opacity: 0, scaleX: 0.2 } : { opacity: 1, scaleX: 1 }}\n            transition={reduzido ? JA : { ...MOLA_FIRME, opacity: SUMIR }}\n          />\n          <motion.path d=\"M4 18h16\" initial={false} animate={state ? { y: -6, rotate: -45 } : { y: 0, rotate: 0 }} transition={t} />\n        </Moldura>\n      );\n\n    case \"play\":\n      return (\n        <Moldura {...props}>\n          {PLAY.map((d, i) => (\n            <motion.path key={i} d={d} fill=\"currentColor\" strokeWidth={1.5} initial={false} animate={{ d: state ? PAUSA[i] : d }} transition={t} />\n          ))}\n        </Moldura>\n      );\n\n    // Cada barra do mais gira as pontas até virar uma perna do check.\n    case \"plus\":\n      return (\n        <Moldura {...props}>\n          <motion.path d=\"M12 5 L12 19\" initial={false} animate={{ d: state ? \"M20 6.5 L9.5 17\" : \"M12 5 L12 19\" }} transition={t} />\n          <motion.path d=\"M5 12 L19 12\" initial={false} animate={{ d: state ? \"M4.5 12 L9.5 17\" : \"M5 12 L19 12\" }} transition={t} />\n        </Moldura>\n      );\n\n    // As folhas se recolhem e o check se escreve, com um respiro curto entre os dois.\n    case \"copy\":\n      return (\n        <Moldura {...props}>\n          <motion.path\n            d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"\n            initial={false}\n            animate={state ? { x: 4, y: 4, opacity: 0 } : { x: 0, y: 0, opacity: 1 }}\n            transition={reduzido ? JA : { ...MOLA_LISA, opacity: SUMIR }}\n          />\n          <motion.rect\n            x=\"8\" y=\"8\" width=\"14\" height=\"14\" rx=\"2\"\n            initial={false}\n            animate={state ? { scale: 0.7, opacity: 0 } : { scale: 1, opacity: 1 }}\n            transition={reduzido ? JA : { ...MOLA_LISA, opacity: SUMIR }}\n          />\n          <motion.path\n            d=\"M5 12.5 L9.5 17 L19 7.5\"\n            initial={false}\n            animate={state ? { pathLength: 1, opacity: 1 } : { pathLength: 0, opacity: 0 }}\n            transition={reduzido ? JA : { ...MOLA_LISA, delay: state ? 0.04 : 0, opacity: { duration: ACENDE_S, delay: state ? 0.04 : 0 } }}\n          />\n        </Moldura>\n      );\n\n    // Sol vira lua: o disco cresce enquanto uma sombra entra pelo canto e cava o\n    // crescente, e os raios giram e se recolhem. O crescente é desenhado como\n    // contorno: a borda do disco fora da sombra mais a borda da sombra dentro do disco.\n    case \"theme\": {\n      const disco = { r: state ? 8.5 : 4.5 };\n      const sombra = state ? { cx: 18, cy: 6, r: 6.5 } : { cx: 30, cy: -6, r: 6.5 };\n      return (\n        <Moldura {...props}>\n          <defs>\n            <mask id={`${id}-mordida`} maskUnits=\"userSpaceOnUse\" x=\"-8\" y=\"-8\" width=\"40\" height=\"40\">\n              <rect x=\"-8\" y=\"-8\" width=\"40\" height=\"40\" fill=\"white\" stroke=\"none\" />\n              <motion.circle cx={30} cy={-6} r={6.5} fill=\"black\" stroke=\"none\" initial={false} animate={sombra} transition={t} />\n            </mask>\n            <clipPath id={`${id}-disco`}>\n              <motion.circle cx={12} cy={12} r={4.5} initial={false} animate={disco} transition={t} />\n            </clipPath>\n          </defs>\n          <motion.circle cx={12} cy={12} r={4.5} mask={`url(#${id}-mordida)`} initial={false} animate={disco} transition={t} />\n          <motion.circle cx={30} cy={-6} r={6.5} clipPath={`url(#${id}-disco)`} initial={false} animate={sombra} transition={t} />\n          <motion.g\n            initial={false}\n            animate={state ? { rotate: 90, scale: 0.4, opacity: 0 } : { rotate: 0, scale: 1, opacity: 1 }}\n            transition={reduzido ? JA : { ...MOLA, opacity: SUMIR }}\n          >\n            {RAIOS.map((d) => (\n              <path key={d} d={d} />\n            ))}\n          </motion.g>\n        </Moldura>\n      );\n    }\n\n    // Seta de abrir e fechar, ou de ordenar: meia volta na mola.\n    case \"chevron\":\n      return (\n        <Moldura {...props}>\n          <motion.path\n            d=\"M6 9 L12 15 L18 9\"\n            initial={false}\n            animate={{ rotate: state ? 180 : 0 }}\n            style={{ transformOrigin: \"50% 50%\" }}\n            transition={t}\n          />\n        </Moldura>\n      );\n  }\n}\n\nexport default IconMorph;\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Botão de ícone com dois estados que viram um no outro, como menu e fechar ou copiar e copiado",
      "evite": "Rótulo de texto trocando: text-morph; tema claro e escuro, eclipse-switch"
    }
  },
  "categories": [
    "controles",
    "icone",
    "botao",
    "estado",
    "micro-interacao"
  ],
  "type": "registry:ui"
}