{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "photo-split",
  "title": "Photo Split",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "A dobra alternada de foto e texto: a moldura revela a imagem, o texto entra deslizando da borda externa e o lado inverte a cada dobra sem ninguém escrever nada.",
  "dependencies": [
    "gsap"
  ],
  "registryDependencies": [
    "utils",
    "https://visant-ui.vercel.app/r/photo-frame.json",
    "https://visant-ui.vercel.app/r/section-intro.json",
    "https://visant-ui.vercel.app/r/fold-motion.json",
    "@visant/photo-frame",
    "@visant/section-intro",
    "@visant/fold-motion"
  ],
  "files": [
    {
      "path": "registry/visant/ui/photo-split.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Fold, FoldPart } from \"@/lib/fold-motion\";\nimport { PhotoFrame } from \"@/components/ui/photo-frame\";\nimport { SectionIntro } from \"@/components/ui/section-intro\";\n\n/**\n * PhotoSplit — a dobra alternada: foto de um lado, argumento do outro.\n *\n * ── Por que ela é composição e quase não tem código próprio ────────────────\n *\n * A dobra foto ↔ texto é o padrão mais copiado da web, e é exatamente por isso\n * que ela costuma sair errada: cada projeto reescreve as três partes dela do\n * zero, e cada reescrita perde uma decisão que a casa já tinha tomado. Aqui as\n * três partes já existem no registry, e esta peça só as arruma:\n *\n * · **`@visant/photo-frame`** revela a foto — a máscara abre e a imagem assenta\n *   mais devagar que a máscara. A proporção declarada é o que impede o salto de\n *   layout que quebraria o gatilho de rolagem.\n * · **`@visant/section-intro`** escreve a coluna de texto, com as DUAS medidas\n *   de linha (título e parágrafo pedem larguras diferentes), `text-balance` no\n *   título e `text-pretty` no lead.\n * · **`@visant/fold-motion`** dá a entrada, na variante `drift`: o texto entra\n *   deslizando da borda externa, e o esconderijo é aplicado pelo componente —\n *   nunca por CSS estático, senão um script que falha apaga a dobra inteira.\n *\n * O que sobra de trabalho próprio é a anatomia, e ela é a parte que se erra:\n *\n * 1. **A alternância não é do consumidor.** Escrever `side=\"left\"` em uma dobra\n *    e `side=\"right\"` na seguinte é uma contabilidade que quebra em silêncio no\n *    dia em que alguém insere uma dobra no meio — e o defeito, duas fotos\n *    seguidas do mesmo lado, é justamente o que a alternância existe pra evitar.\n *    `PhotoSplitStack` escreve o lado de cada filha, igual ao `NavDrawerList`\n *    escrevendo o índice da cascata. Quem quiser mandar continua mandando: um\n *    `side` explícito na filha ganha do automático.\n * 2. **A inversão é `flex-row-reverse`, não `order`.** Com `order`, a dobra\n *    invertida empilharia o texto ANTES da foto no celular — metade das dobras\n *    da página com uma ordem e metade com a outra. Em `row-reverse` com\n *    `flex-wrap`, a foto continua vindo primeiro quando empilha, sempre.\n * 3. **A quebra é do CONTAINER.** Nenhum `sm:`/`lg:` no arquivo: as duas colunas\n *    têm base mínima e o `flex-wrap` decide. Uma dobra montada dentro de uma\n *    coluna de 700px empilha; a mesma dobra em página inteira não. Media query\n *    de viewport erraria as duas.\n * 4. **A entrada vem do lado de FORA.** Foto à esquerda → o texto entra pela\n *    direita. Convergir para o centro é o que faz a dobra parecer fechar; entrar\n *    os dois do mesmo lado faz a página parecer escorregar.\n *\n * `prefers-reduced-motion` é herdado das três peças: a moldura não mascara nem\n * deriva, e a dobra nasce no estado final. Zera o movimento, nunca o resultado.\n */\n\nexport interface PhotoSplitProps\n  extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n  /** A mídia, por slot: `<img>`, `next/image`, `<video>`. */\n  media: React.ReactNode;\n  title: React.ReactNode;\n  /** O rótulo acima do título. Decorativo — vai com `aria-hidden`. */\n  eyebrow?: React.ReactNode;\n  lead?: React.ReactNode;\n  /** Botões e links abaixo do texto. */\n  actions?: React.ReactNode;\n  /** De que lado fica a FOTO. Dentro de `PhotoSplitStack` é escrito automático. */\n  side?: \"left\" | \"right\";\n  /** Proporção da moldura, formato CSS. */\n  ratio?: string;\n  /** Deriva da foto na rolagem, em % da altura. 0 desliga. */\n  parallax?: number;\n  /** Legenda da foto. Entra como `<figcaption>` da moldura. */\n  caption?: React.ReactNode;\n  /** Respiro entre as duas colunas, em rem. */\n  gap?: number;\n  /** Peso da coluna da foto contra a do texto. 1 é meio a meio. */\n  weight?: number;\n  as?: \"h2\" | \"h3\";\n}\n\nexport function PhotoSplit({\n  media,\n  title,\n  eyebrow,\n  lead,\n  actions,\n  side = \"left\",\n  ratio = \"4/5\",\n  parallax = 0,\n  caption,\n  gap = 3.5,\n  weight = 1.1,\n  as = \"h2\",\n  className,\n  style,\n  children,\n  ...props\n}: PhotoSplitProps) {\n  return (\n    <Fold\n      as=\"section\"\n      // A dobra INTEIRA é o flex, e não um filho dela: o `fold-motion` move\n      // `> [data-fold-part]`, filho direto. Com um invólucro no meio a variante\n      // `drift` deixaria de encontrar a coluna de texto e a entrada sumiria sem\n      // erro nenhum no console.\n      variant={side === \"left\" ? \"drift-r\" : \"drift-l\"}\n      className={cn(\n        \"flex flex-wrap items-center\",\n        side === \"right\" && \"flex-row-reverse\",\n        className,\n      )}\n      // O `style` de quem chama vem depois: o respiro é padrão da peça, não\n      // decisão dela sobre o que o consumidor pode ajustar.\n      style={{ gap: `${gap}rem`, ...style }}\n      {...props}\n    >\n      <div\n        className=\"min-w-0 basis-[24rem]\"\n        style={{ flexGrow: weight, flexShrink: 1 }}\n      >\n        <PhotoFrame\n          ratio={ratio}\n          parallax={parallax}\n          caption={caption}\n          // A moldura abre do lado de FORA da dobra pra dentro, acompanhando o\n          // mesmo sentido de convergência da entrada do texto.\n          from={side === \"left\" ? \"left\" : \"right\"}\n        >\n          {media}\n        </PhotoFrame>\n      </div>\n\n      <FoldPart\n        className={cn(\n          \"flex min-w-0 flex-1 basis-[20rem] flex-col gap-6\",\n          // O clamp do `section-intro` é calibrado pra dobra de largura inteira:\n          // numa coluna que vale metade da página, a mesma `vw` entrega um título\n          // grande demais pro espaço que ele tem. O teto cai aqui, e só o teto —\n          // o piso e a curva continuam sendo os do sistema.\n          \"[&_:is(h1,h2,h3)]:text-[clamp(1.625rem,1.25rem+1.5vw,2.5rem)]\",\n        )}\n      >\n        <SectionIntro\n          align=\"start\"\n          as={as}\n          eyebrow={eyebrow}\n          title={title}\n          lead={lead}\n          actions={actions}\n          // Medidas menores que o padrão da peça: a coluna aqui é estreita, e a\n          // medida de linha tem que caber nela em vez de ser cortada pelo flex.\n          titleWidth={17}\n          leadWidth={46}\n        />\n        {children}\n      </FoldPart>\n    </Fold>\n  );\n}\n\nexport interface PhotoSplitStackProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** De que lado começa a primeira foto. As seguintes alternam sozinhas. */\n  start?: \"left\" | \"right\";\n  /** Respiro entre as dobras, em rem. */\n  space?: number;\n}\n\n/**\n * A pilha que escreve o lado de cada dobra.\n *\n * Clonar e não passar por contexto: o lado precisa chegar como PROP, porque é\n * ele que decide a variante de entrada e o sentido da abertura da moldura — e\n * as duas coisas são lidas no render da filha, não num efeito.\n */\nexport function PhotoSplitStack({\n  start = \"left\",\n  space = 6,\n  className,\n  style,\n  children,\n  ...props\n}: PhotoSplitStackProps) {\n  const itens = React.Children.toArray(children).filter(React.isValidElement);\n\n  return (\n    <div\n      className={cn(\"flex flex-col\", className)}\n      style={{ gap: `${space}rem`, ...style }}\n      {...props}\n    >\n      {itens.map((child, i) => {\n        const filha = child as React.ReactElement<PhotoSplitProps>;\n        const alternado: PhotoSplitProps[\"side\"] =\n          (i % 2 === 0) === (start === \"left\") ? \"left\" : \"right\";\n        return React.cloneElement(filha, {\n          // Um `side` escrito à mão na filha ganha: a pilha resolve o caso\n          // normal, não impõe uma regra que não se pode quebrar.\n          side: filha.props.side ?? alternado,\n        });\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Seções alternadas de foto e texto em landing ou case, invertendo o lado a cada dobra",
      "evite": "Foto sozinha com reveal: photo-frame; arte presa avançando em passos, pinned-scene"
    }
  },
  "categories": [
    "conteudo",
    "dobra",
    "secao",
    "foto",
    "texto",
    "alternado",
    "scroll",
    "gsap"
  ],
  "type": "registry:ui"
}