{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pix-automatico",
  "title": "Pix Automático",
  "description": "The authorization step for recurring Pix: clear terms, QR Code or bank-app confirmation with a bank picker, a live waiting state that names the bank, and an active view with the next charges.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "input",
    "popover",
    "toggle",
    "https://uaiblocks.dev/r/uai-number-tick.json",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/pix-automatico.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  Check,\n  ChevronLeft,\n  Copy,\n  Info,\n  LoaderCircle,\n  QrCode,\n  Repeat,\n  Search,\n  SearchX,\n  Smartphone,\n  X,\n} from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type KeyboardEvent,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { toggleVariants } from \"@/components/ui/toggle\";\nimport { NumberTick } from \"@/lib/uai-number-tick\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const PIX_AUTOMATICO_VARIANTS = [\"card\", \"plain\", \"compact\"] as const;\nexport const PIX_AUTOMATICO_STATUSES = [\n  \"review\",\n  \"awaiting\",\n  \"active\",\n  \"declined\",\n  \"expired\",\n  \"cancelled\",\n] as const;\nexport const PIX_AUTOMATICO_METHODS = [\"qr\", \"app\"] as const;\nexport const PIX_AUTOMATICO_PERIODS = [\"week\", \"month\", \"quarter\", \"semester\", \"year\"] as const;\n\nexport type PixAutomaticoVariant = (typeof PIX_AUTOMATICO_VARIANTS)[number];\nexport type PixAutomaticoStatus = (typeof PIX_AUTOMATICO_STATUSES)[number];\nexport type PixAutomaticoMethod = (typeof PIX_AUTOMATICO_METHODS)[number];\nexport type PixAutomaticoPeriod = (typeof PIX_AUTOMATICO_PERIODS)[number];\nexport type PixAutomaticoCharge = { date: Date | string; amount: number };\n\nexport type PixAutomaticoBank = {\n  /** Stable. Drives the monogram tone and `recentBankId`. */\n  id: string;\n  /** The full, accessible name: \"Banco do Brasil\". */\n  name: string;\n  /** Used in copy such as \"Enviar para o app {shortName}\". Falls back to `name`. */\n  shortName?: string;\n  /** From assets you're licensed to use. Rendered as a decorative image; the monogram on error. */\n  logoUrl?: string;\n  /** The 8-digit participant code. Passed through untouched, never shown. */\n  ispb?: string;\n  /** Overrides the derived initials (max 2 characters). */\n  monogram?: string;\n  /** Extra search terms, such as [\"bb\"] or [\"cef\", \"caixa economica federal\"]. */\n  keywords?: readonly string[];\n  disabled?: boolean;\n  /** Shown on a disabled row. Defaults to \"Indisponível\". */\n  disabledReason?: string;\n};\n\nexport type PixAutomaticoBanksStatus = \"ready\" | \"loading\" | \"error\";\n\nexport type PixAutomaticoAuthorizePayload =\n  | { method: \"qr\" }\n  | { method: \"app\"; bank: PixAutomaticoBank };\n\n/**\n * Example data, not a registry. Names identify the institutions (nominative use); no logos or\n * trademarks are bundled, so pass `logoUrl` from assets you're licensed to use. Pix Automático\n * participation changes, so real integrations should pass their payment provider's participants\n * through `banks`.\n */\nexport const PIX_AUTOMATICO_DEFAULT_BANKS: readonly PixAutomaticoBank[] = [\n  { id: \"nubank\", name: \"Nubank\", monogram: \"NU\", keywords: [\"nu\"] },\n  { id: \"itau\", name: \"Itaú\", monogram: \"IT\", keywords: [\"itau\", \"itau unibanco\"] },\n  { id: \"bb\", name: \"Banco do Brasil\", monogram: \"BB\", keywords: [\"bb\"] },\n  { id: \"caixa\", name: \"Caixa\", monogram: \"CX\", keywords: [\"cef\", \"caixa economica federal\"] },\n  { id: \"bradesco\", name: \"Bradesco\", monogram: \"BR\" },\n  { id: \"santander\", name: \"Santander\", monogram: \"SA\" },\n  { id: \"inter\", name: \"Inter\", monogram: \"IN\", keywords: [\"banco inter\"] },\n  { id: \"c6\", name: \"C6 Bank\", monogram: \"C6\" },\n  { id: \"mercadopago\", name: \"Mercado Pago\", monogram: \"MP\" },\n  { id: \"picpay\", name: \"PicPay\", monogram: \"PP\" },\n  { id: \"btg\", name: \"BTG Pactual\", monogram: \"BT\" },\n  { id: \"sicoob\", name: \"Sicoob\", monogram: \"SC\" },\n  { id: \"sicredi\", name: \"Sicredi\", monogram: \"SI\" },\n  { id: \"pagbank\", name: \"PagBank\", monogram: \"PB\", keywords: [\"pagseguro\"] },\n  { id: \"banrisul\", name: \"Banrisul\", monogram: \"BA\" },\n];\n\n/** Lowercase, without accents: \"Itaú\" → \"itau\". */\nfunction fold(value: string) {\n  return value\n    .normalize(\"NFD\")\n    .replace(/\\p{Diacritic}/gu, \"\")\n    .toLowerCase();\n}\n\nconst MONOGRAM_STOPWORDS = new Set([\"banco\", \"de\", \"do\", \"da\", \"dos\", \"das\", \"e\"]);\n\n/** The bank's initials: `monogram`, or two letters derived from its name. */\nexport function pixAutomaticoBankMonogram(bank: PixAutomaticoBank) {\n  if (bank.monogram) return bank.monogram.slice(0, 2).toUpperCase();\n  const words = fold(bank.shortName ?? bank.name)\n    .split(/\\s+/)\n    .map((word) => word.replace(/[^a-z0-9]/g, \"\"))\n    .filter((word) => word && !MONOGRAM_STOPWORDS.has(word));\n  const initials =\n    words.length >= 2 ? `${words[0]?.[0]}${words[1]?.[0]}` : (words[0] ?? \"\").slice(0, 2);\n  return initials.toUpperCase();\n}\n\nexport const PIX_AUTOMATICO_BANK_TONES = [\"neutral\", \"primary\", \"success\", \"warning\"] as const;\nexport type PixAutomaticoBankTone = (typeof PIX_AUTOMATICO_BANK_TONES)[number];\n\n/** A stable tone from the bank id (FNV-1a), never a brand color. */\nexport function pixAutomaticoBankTone(id: string): PixAutomaticoBankTone {\n  let hash = 0x811c9dc5;\n  for (let index = 0; index < id.length; index++) {\n    hash ^= id.charCodeAt(index);\n    hash = Math.imul(hash, 0x01000193) >>> 0;\n  }\n  return PIX_AUTOMATICO_BANK_TONES[hash % 4] as PixAutomaticoBankTone;\n}\n\nfunction bankShortName(bank: PixAutomaticoBank) {\n  return bank.shortName ?? bank.name;\n}\n\n/** 0 when the text starts with the needle, 1 when a word does, 2 for any substring. */\nfunction matchIn(text: string, needle: string): { tier: number; at: number } | null {\n  let substring = -1;\n  for (let at = text.indexOf(needle); at !== -1; at = text.indexOf(needle, at + 1)) {\n    if (at === 0) return { tier: 0, at };\n    if (text[at - 1] === \" \") return { tier: 1, at };\n    if (substring === -1) substring = at;\n  }\n  return substring === -1 ? null : { tier: 2, at: substring };\n}\n\ntype BankMatch = { bank: PixAutomaticoBank; tier: number; highlight: number };\n\n/**\n * Name-start, then word-start, then substring matches, each tier kept in `banks` order. Matches\n * that only hit `keywords` come last, so \"ban\" lists Banco do Brasil before Inter (\"banco inter\").\n */\nfunction searchBanks(banks: readonly PixAutomaticoBank[], query: string): BankMatch[] {\n  const needle = fold(query.trim());\n  const matches: BankMatch[] = [];\n  for (const bank of banks) {\n    const inName = matchIn(fold(bank.name), needle);\n    const inShort = bank.shortName ? matchIn(fold(bank.shortName), needle) : null;\n    const inKeywords = (bank.keywords ?? []).some((keyword) => fold(keyword).includes(needle));\n    const tier = Math.min(inName?.tier ?? 9, inShort?.tier ?? 9, inKeywords ? 3 : 9);\n    if (tier < 9) matches.push({ bank, tier, highlight: inName?.at ?? -1 });\n  }\n  return matches.sort((first, second) => first.tier - second.tier);\n}\n\nexport function formatBrl(value: number) {\n  return new Intl.NumberFormat(\"pt-BR\", { style: \"currency\", currency: \"BRL\" }).format(value);\n}\n\nconst PERIOD_LABELS: Record<PixAutomaticoPeriod, string> = {\n  week: \"por semana\",\n  month: \"por mês\",\n  quarter: \"por trimestre\",\n  semester: \"por semestre\",\n  year: \"por ano\",\n};\n\n/** \"por mês\", \"por semana\"… */\nexport function pixAutomaticoPeriodLabel(period: PixAutomaticoPeriod) {\n  return PERIOD_LABELS[period];\n}\n\nconst MONTHS_SHORT = [\n  \"jan\",\n  \"fev\",\n  \"mar\",\n  \"abr\",\n  \"mai\",\n  \"jun\",\n  \"jul\",\n  \"ago\",\n  \"set\",\n  \"out\",\n  \"nov\",\n  \"dez\",\n];\nconst MONTHS_LONG = [\n  \"janeiro\",\n  \"fevereiro\",\n  \"março\",\n  \"abril\",\n  \"maio\",\n  \"junho\",\n  \"julho\",\n  \"agosto\",\n  \"setembro\",\n  \"outubro\",\n  \"novembro\",\n  \"dezembro\",\n];\n\n/** Reads a charge date as a calendar day. \"2026-11-05\" stays the 5th in every time zone. */\nfunction calendarDay(value: Date | string) {\n  if (typeof value === \"string\") {\n    const match = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(value);\n    if (match) return { day: Number(match[3]), month: Number(match[2]) - 1 };\n  }\n  const date = new Date(value);\n  return { day: date.getDate(), month: date.getMonth() };\n}\n\nfunction toTime(value: Date | string | number) {\n  return new Date(value).getTime();\n}\n\nfunction formatClock(seconds: number) {\n  const minutes = Math.floor(seconds / 60);\n  return `${String(minutes).padStart(2, \"0\")}:${String(seconds % 60).padStart(2, \"0\")}`;\n}\n\nconst STATUS_LABELS: Record<PixAutomaticoStatus, string> = {\n  review: \"Pix Automático\",\n  awaiting: \"Aguardando\",\n  active: \"Ativo\",\n  declined: \"Recusado\",\n  expired: \"Expirado\",\n  cancelled: \"Cancelado\",\n};\n\nconst STATUS_SPOKEN: Partial<Record<PixAutomaticoStatus, string>> = {\n  awaiting: \"Aguardando confirmação no banco\",\n  active: \"Pix Automático autorizado\",\n  declined: \"Autorização recusada\",\n};\n\ntype PixAutomaticoStep = \"terms\" | \"bank\";\n\ntype PixAutomaticoContextValue = {\n  variant: PixAutomaticoVariant;\n  status: PixAutomaticoStatus;\n  method: PixAutomaticoMethod;\n  setMethod: (method: PixAutomaticoMethod) => void;\n  /** Internal: which review step shows. Only meaningful while status is \"review\". */\n  step: PixAutomaticoStep;\n  /** Opens the bank step. Focus returns to `opener` (or the checked method) on Back. */\n  openBankPicker: (opener?: HTMLElement | null) => void;\n  closeBankPicker: () => void;\n  banks: readonly PixAutomaticoBank[];\n  banksStatus: PixAutomaticoBanksStatus;\n  onBanksRetry: (() => void) | undefined;\n  recentBank: PixAutomaticoBank | null;\n  bank: PixAutomaticoBank | null;\n  selectBank: (bank: PixAutomaticoBank) => void;\n  changeBank: (() => void) | undefined;\n  pending: boolean;\n  announce: (message: string) => void;\n  waitingTitleId: string;\n  secondsLeft: number | null;\n  code: string | undefined;\n  copied: boolean;\n  copyFailed: boolean;\n  copy: () => Promise<void>;\n  titleId: string;\n  authorize: () => void;\n  sendToBank: () => void;\n  onRetry: (() => void) | undefined;\n  onOtherMethod: (() => void) | undefined;\n};\n\nconst PixAutomaticoContext = createContext<PixAutomaticoContextValue | null>(null);\n\nfunction usePixAutomatico(name: string) {\n  const context = useContext(PixAutomaticoContext);\n  if (!context) throw new Error(`${name} must be used within PixAutomatico`);\n  return context;\n}\n\nconst rootVariants = cva(\n  \"@container/pa flex w-full min-w-0 flex-col text-[13px]/[18px] text-card-foreground [transition:border-color_240ms_ease-out] motion-reduce:transition-none\",\n  {\n    variants: {\n      variant: {\n        card: \"max-w-[380px] gap-4 rounded-[14px] border border-border bg-card p-5\",\n        plain: \"gap-4\",\n        compact: \"max-w-[320px] gap-3 rounded-[12px] border border-border bg-card p-3.5\",\n      },\n    },\n  },\n);\n\nexport type PixAutomaticoProps = Omit<ComponentProps<\"section\">, \"children\"> & {\n  variant?: PixAutomaticoVariant;\n  /** Authorization state owned by the application, from the PSP's webhook or polling. */\n  status?: PixAutomaticoStatus;\n  method?: PixAutomaticoMethod;\n  defaultMethod?: PixAutomaticoMethod;\n  onMethodChange?: (method: PixAutomaticoMethod) => void;\n  /** The QR Code's copy-and-paste payload, for Copiar código. */\n  code?: string;\n  /** When the authorization request expires. The countdown ticks while awaiting. */\n  expiresAt?: Date | string | number;\n  /** Fixes the starting clock so server and client render the same countdown. */\n  now?: Date | string | number;\n  /** Receives the chosen bank for the app method. Send the request, then set status=\"awaiting\". */\n  onAuthorize?: (payload: PixAutomaticoAuthorizePayload) => void;\n  /** The bank list, in display order. Defaults to the example list in PIX_AUTOMATICO_DEFAULT_BANKS. */\n  banks?: readonly PixAutomaticoBank[];\n  banksStatus?: PixAutomaticoBanksStatus;\n  /** Shows Tentar de novo when the list fails to load. */\n  onBanksRetry?: () => void;\n  /** Pinned under \"Usado da última vez\" and selected by default. The component never stores it. */\n  recentBankId?: string;\n  bankId?: string | null;\n  defaultBankId?: string | null;\n  onBankSelect?: (bank: PixAutomaticoBank) => void;\n  /** Fires when the bank picker opens or closes, for UI around the card that follows it. */\n  onBankPickerOpenChange?: (open: boolean) => void;\n  /** Shows Trocar banco while awaiting: cancel the open request, then set status=\"review\". */\n  onChangeBank?: () => void;\n  /** The request is in flight: the bank step locks and its button reads Enviando…. */\n  pending?: boolean;\n  onRetry?: () => void;\n  /** Outra forma de pagamento, after a decline. */\n  onOtherMethod?: () => void;\n  onExpire?: () => void;\n  onCodeCopy?: (code: string) => void;\n  children?: ReactNode;\n};\n\n/**\n * The authorization step for recurring Pix. It shows terms and status only: the payer sets any\n * maximum amount in the bank app, never here.\n */\nexport function PixAutomatico({\n  variant = \"card\",\n  status = \"review\",\n  method: methodProp,\n  defaultMethod = \"qr\",\n  onMethodChange,\n  code,\n  expiresAt,\n  now,\n  onAuthorize,\n  banks = PIX_AUTOMATICO_DEFAULT_BANKS,\n  banksStatus = \"ready\",\n  onBanksRetry,\n  recentBankId,\n  bankId: bankIdProp,\n  defaultBankId = recentBankId ?? null,\n  onBankSelect,\n  onBankPickerOpenChange,\n  onChangeBank,\n  pending = false,\n  onRetry,\n  onOtherMethod,\n  onExpire,\n  onCodeCopy,\n  children,\n  className,\n  ...props\n}: PixAutomaticoProps) {\n  const [internalMethod, setInternalMethod] = useState<PixAutomaticoMethod>(defaultMethod);\n  const method = methodProp ?? internalMethod;\n  const [internalBankId, setInternalBankId] = useState<string | null>(defaultBankId);\n  const bankId = bankIdProp === undefined ? internalBankId : bankIdProp;\n  const bank = banks.find((item) => item.id === bankId) ?? null;\n  const recentBank = banks.find((item) => item.id === recentBankId) ?? null;\n  const [step, setStep] = useState<PixAutomaticoStep>(\"terms\");\n  const opener = useRef<HTMLElement | null>(null);\n  const returnFocus = useRef(false);\n  const previousStatus = useRef(status);\n  const waitingTitleId = useId();\n  const deadline = expiresAt === undefined ? null : toTime(expiresAt);\n  // A fixed `now` becomes an offset from the real clock, so the countdown keeps ticking.\n  const [offset] = useState(() => (now === undefined ? 0 : toTime(now) - Date.now()));\n  const [clock, setClock] = useState(() => (now === undefined ? Date.now() : toTime(now)));\n  const [copied, setCopied] = useState(false);\n  const [copyFailed, setCopyFailed] = useState(false);\n  const [announcement, setAnnouncement] = useState(\"\");\n  const expiredNotified = useRef(false);\n  const minuteNotified = useRef(false);\n  const onExpireRef = useRef(onExpire);\n  onExpireRef.current = onExpire;\n  const titleId = useId();\n\n  const awaiting = status === \"awaiting\";\n  const secondsLeft = deadline === null ? null : Math.max(0, Math.ceil((deadline - clock) / 1000));\n\n  useEffect(() => {\n    if (!awaiting || deadline === null) return;\n    setClock(Date.now() + offset);\n    const timer = window.setInterval(() => setClock(Date.now() + offset), 1000);\n    return () => window.clearInterval(timer);\n  }, [awaiting, deadline, offset]);\n\n  useEffect(() => {\n    if (!awaiting || secondsLeft === null) return;\n    if (secondsLeft === 0 && !expiredNotified.current) {\n      expiredNotified.current = true;\n      onExpireRef.current?.();\n    }\n    if (secondsLeft > 0 && secondsLeft <= 60 && !minuteNotified.current) {\n      minuteNotified.current = true;\n      setAnnouncement(\"Falta 1 minuto para o pedido expirar\");\n    }\n  }, [awaiting, secondsLeft]);\n\n  // A new request (back to review) re-arms the one-shot notices.\n  useEffect(() => {\n    if (status !== \"review\") return;\n    expiredNotified.current = false;\n    minuteNotified.current = false;\n    setAnnouncement(\"\");\n  }, [status]);\n\n  // Leaving review closes the bank step, and a sent request moves focus to the waiting title so it\n  // isn't lost to <body> when the review goes inert. Trocar banco reopens the step on its own.\n  useEffect(() => {\n    const previous = previousStatus.current;\n    previousStatus.current = status;\n    if (status !== \"review\") setStep((current) => (previous === \"review\" ? \"terms\" : current));\n    if (previous === \"review\" && status === \"awaiting\") {\n      document.getElementById(waitingTitleId)?.focus();\n    }\n  }, [status, waitingTitleId]);\n\n  // The picker shows only on the review step, so leaving review closes it for the consumer too.\n  const bankPickerOpen = status === \"review\" && step === \"bank\";\n  const bankPickerWasOpen = useRef(bankPickerOpen);\n  const onBankPickerOpenChangeRef = useRef(onBankPickerOpenChange);\n  onBankPickerOpenChangeRef.current = onBankPickerOpenChange;\n  useEffect(() => {\n    if (bankPickerWasOpen.current === bankPickerOpen) return;\n    bankPickerWasOpen.current = bankPickerOpen;\n    onBankPickerOpenChangeRef.current?.(bankPickerOpen);\n  }, [bankPickerOpen]);\n\n  // Back returns focus to whatever opened the step.\n  useEffect(() => {\n    if (step !== \"terms\" || !returnFocus.current) return;\n    returnFocus.current = false;\n    const target = opener.current?.isConnected\n      ? opener.current\n      : document\n          .getElementById(titleId)\n          ?.closest(\"section\")\n          ?.querySelector<HTMLElement>('[role=\"radio\"][aria-checked=\"true\"]');\n    target?.focus();\n  }, [step, titleId]);\n\n  useEffect(() => {\n    if (!copied) return;\n    const timer = window.setTimeout(() => setCopied(false), 2000);\n    return () => window.clearTimeout(timer);\n  }, [copied]);\n\n  const setMethod = (next: PixAutomaticoMethod) => {\n    if (methodProp === undefined) setInternalMethod(next);\n    onMethodChange?.(next);\n  };\n\n  const openBankPicker = (from?: HTMLElement | null) => {\n    opener.current = from ?? null;\n    setStep(\"bank\");\n  };\n\n  const closeBankPicker = () => {\n    returnFocus.current = true;\n    setStep(\"terms\");\n  };\n\n  const selectBank = (next: PixAutomaticoBank) => {\n    if (next.disabled || pending) return;\n    if (bankIdProp === undefined) setInternalBankId(next.id);\n    onBankSelect?.(next);\n  };\n\n  const sendToBank = () => {\n    if (bank && !bank.disabled && !pending) onAuthorize?.({ method: \"app\", bank });\n  };\n\n  const copy = async () => {\n    if (!code) return;\n    try {\n      await navigator.clipboard.writeText(code);\n      setCopyFailed(false);\n      setCopied(true);\n      onCodeCopy?.(code);\n    } catch {\n      setCopied(false);\n      setCopyFailed(true);\n    }\n  };\n\n  return (\n    <PixAutomaticoContext.Provider\n      value={{\n        variant,\n        status,\n        method,\n        setMethod,\n        secondsLeft,\n        code,\n        copied,\n        copyFailed,\n        copy,\n        titleId,\n        step,\n        openBankPicker,\n        closeBankPicker,\n        banks,\n        banksStatus,\n        onBanksRetry,\n        recentBank,\n        bank,\n        selectBank,\n        changeBank: onChangeBank\n          ? () => {\n              opener.current = null;\n              setStep(\"bank\");\n              onChangeBank();\n            }\n          : undefined,\n        pending,\n        announce: setAnnouncement,\n        waitingTitleId,\n        authorize: () => {\n          if (method === \"qr\") onAuthorize?.({ method: \"qr\" });\n          else if (bank) sendToBank();\n          else openBankPicker(document.activeElement as HTMLElement | null);\n        },\n        sendToBank,\n        onRetry,\n        onOtherMethod,\n      }}\n    >\n      <section\n        data-slot=\"pix-automatico\"\n        data-variant={variant}\n        data-status={status}\n        data-method={method}\n        aria-labelledby={titleId}\n        className={cn(\n          rootVariants({ variant }),\n          status === \"declined\" && variant !== \"plain\" && \"border-destructive/40\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <span role=\"status\" className=\"sr-only\">\n          {announcement}\n        </span>\n      </section>\n    </PixAutomaticoContext.Provider>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Header\n * -----------------------------------------------------------------------------------------------*/\n\nexport function PixAutomaticoHeader({ className, ...props }: ComponentProps<\"header\">) {\n  return (\n    <header\n      data-slot=\"pix-automatico-header\"\n      className={cn(\"flex min-w-0 items-center gap-2.5\", className)}\n      {...props}\n    />\n  );\n}\n\n/** The recipient's initials in a 32px circle (28px in compact). */\nexport function PixAutomaticoAvatar({ className, ...props }: ComponentProps<\"span\">) {\n  const context = usePixAutomatico(\"PixAutomaticoAvatar\");\n  return (\n    <span\n      data-slot=\"pix-automatico-avatar\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"grid shrink-0 place-items-center rounded-full bg-muted text-[11.5px] font-semibold text-foreground shadow-[0_0_0_1px_var(--border)]\",\n        context.variant === \"compact\" ? \"size-7\" : \"size-8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** Product and recipient lines. The product names the section. */\nexport function PixAutomaticoTitle({\n  product,\n  recipient,\n  className,\n  ...props\n}: Omit<ComponentProps<\"div\">, \"children\"> & { product: ReactNode; recipient?: ReactNode }) {\n  const context = usePixAutomatico(\"PixAutomaticoTitle\");\n  return (\n    <div\n      data-slot=\"pix-automatico-title\"\n      className={cn(\"grid min-w-0 flex-1\", className)}\n      {...props}\n    >\n      <h3\n        id={context.titleId}\n        className=\"m-0 truncate text-[13px]/[18px] font-medium text-foreground\"\n      >\n        {product}\n      </h3>\n      {recipient ? (\n        <p className=\"m-0 truncate text-[12px]/4 text-subtle-foreground tabular-nums\">\n          {recipient}\n        </p>\n      ) : null}\n    </div>\n  );\n}\n\n/** A polite live region: the short label shows, the full sentence is spoken. */\nexport function PixAutomaticoStatusBadge({\n  className,\n  children,\n  ...props\n}: ComponentProps<\"span\">) {\n  const context = usePixAutomatico(\"PixAutomaticoStatusBadge\");\n  const { status } = context;\n  const spoken =\n    status === \"awaiting\" && context.method === \"app\" && context.bank\n      ? `Pedido enviado para o app ${bankShortName(context.bank)}. Aguardando confirmação.`\n      : STATUS_SPOKEN[status];\n  return (\n    <Badge\n      key={status === \"declined\" ? \"declined\" : \"badge\"}\n      variant=\"secondary\"\n      data-slot=\"pix-automatico-status-badge\"\n      data-status={status}\n      className={cn(\n        \"h-6 shrink-0 gap-1.5 rounded-full border-0 px-2.5 py-0 text-[11.5px] font-medium\",\n        context.variant === \"compact\" && \"h-[22px] px-2 text-[11px]\",\n        (status === \"review\" || status === \"expired\" || status === \"cancelled\") &&\n          \"bg-muted text-muted-foreground\",\n        status === \"awaiting\" && \"bg-warning/14 text-warning-ink\",\n        status === \"active\" && \"bg-success/14 text-success-ink\",\n        status === \"declined\" &&\n          \"bg-destructive/14 text-destructive-ink motion-safe:animate-shake motion-reduce:animate-none\",\n        className,\n      )}\n      {...props}\n      role=\"status\"\n      aria-live=\"polite\"\n    >\n      {status === \"review\" ? (\n        <Repeat className=\"size-3\" strokeWidth={2} aria-hidden=\"true\" />\n      ) : status === \"active\" ? (\n        <Check className=\"size-3\" strokeWidth={2.2} aria-hidden=\"true\" />\n      ) : (\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"size-1.5 rounded-full bg-current\",\n            status === \"awaiting\" && \"motion-safe:animate-pulse\",\n          )}\n        />\n      )}\n      {children ?? (\n        <>\n          <span aria-hidden={spoken ? true : undefined}>{STATUS_LABELS[status]}</span>\n          {spoken ? <span className=\"sr-only\">{spoken}</span> : null}\n        </>\n      )}\n    </Badge>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Review: amount, terms, notice, method, authorize\n * -----------------------------------------------------------------------------------------------*/\n\nconst STEP_FLOOR: Record<PixAutomaticoVariant, number> = { card: 340, plain: 340, compact: 300 };\n\n/** The terms leave 8px left in 160ms; they come back in 240ms after a 60ms beat. */\nconst termsLeave =\n  \"-translate-x-2 opacity-0 [transition:opacity_160ms_cubic-bezier(0.23,1,0.32,1),translate_160ms_cubic-bezier(0.23,1,0.32,1)]\";\nconst termsEnter =\n  \"[transition:opacity_240ms_cubic-bezier(0.23,1,0.32,1)_60ms,translate_240ms_cubic-bezier(0.23,1,0.32,1)_60ms]\";\n\n/**\n * Holds the review content. It collapses with grid rows once the request is sent. The terms and\n * the bank step share one grid cell, so opening the picker keeps the card's height.\n */\nexport function PixAutomaticoReview({ className, children, ...props }: ComponentProps<\"div\">) {\n  const context = usePixAutomatico(\"PixAutomaticoReview\");\n  const open = context.status === \"review\";\n  const picking = open && context.step === \"bank\";\n  const terms = useRef<HTMLDivElement>(null);\n  const [termsHeight, setTermsHeight] = useState(0);\n\n  // Lock the bank step to the terms' height, re-measured while it's open (container or font zoom).\n  useLayoutEffect(() => {\n    const node = terms.current;\n    if (!picking || !node) return;\n    const measure = () => setTermsHeight(node.getBoundingClientRect().height);\n    measure();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [picking]);\n\n  return (\n    <div\n      data-slot=\"pix-automatico-review\"\n      inert={!open || undefined}\n      aria-hidden={!open || undefined}\n      className={cn(\n        \"grid min-w-0 [transition:grid-template-rows_300ms_cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\",\n        open ? \"grid-rows-[1fr]\" : \"-mt-4 grid-rows-[0fr]\",\n        context.variant === \"compact\" && !open && \"-mt-3\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"grid min-h-0 min-w-0 overflow-hidden\">\n        <div\n          ref={terms}\n          inert={picking || undefined}\n          className={cn(\n            \"grid min-w-0 self-start\",\n            context.variant === \"compact\" ? \"gap-3\" : \"gap-4\",\n            \"[grid-area:1/1] motion-reduce:transition-none\",\n            picking ? termsLeave : termsEnter,\n          )}\n        >\n          {children}\n        </div>\n        {picking ? (\n          <PixAutomaticoBankPicker\n            style={{ height: Math.max(termsHeight, STEP_FLOOR[context.variant]) }}\n          />\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\nexport type PixAutomaticoAmountProps = Omit<ComponentProps<\"p\">, \"children\"> & {\n  value: number | \"variable\";\n  period?: PixAutomaticoPeriod;\n};\n\n/** \"R$ 89,90 por mês\", or \"Valor variável\". */\nexport function PixAutomaticoAmount({\n  value,\n  period,\n  className,\n  ...props\n}: PixAutomaticoAmountProps) {\n  const context = usePixAutomatico(\"PixAutomaticoAmount\");\n  const compact = context.variant === \"compact\";\n  return (\n    <p\n      data-slot=\"pix-automatico-amount\"\n      className={cn(\"m-0 flex min-w-0 flex-wrap items-baseline gap-x-1\", className)}\n      {...props}\n    >\n      <span\n        className={cn(\n          \"font-semibold tracking-[-0.01em] text-foreground tabular-nums\",\n          value === \"variable\"\n            ? compact\n              ? \"text-[18px]/6\"\n              : \"text-[22px]/7\"\n            : compact\n              ? \"text-[20px]/6\"\n              : \"text-[28px]/8\",\n        )}\n      >\n        {value === \"variable\" ? \"Valor variável\" : formatBrl(value)}\n      </span>\n      {period && value !== \"variable\" ? (\n        <span className=\"text-[13px]/[18px] text-subtle-foreground\">{PERIOD_LABELS[period]}</span>\n      ) : null}\n    </p>\n  );\n}\n\nexport function PixAutomaticoTerms({ className, ...props }: ComponentProps<\"dl\">) {\n  return (\n    <dl\n      data-slot=\"pix-automatico-terms\"\n      className={cn(\n        \"m-0 grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-2 text-[12.5px]/[18px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport type PixAutomaticoTermProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  label: ReactNode;\n  children: ReactNode;\n  /** A second line under the value, such as the CNPJ. */\n  detail?: ReactNode;\n};\n\n/** One row: Primeira cobrança, Periodicidade, Vigência, Recebedor, Valor máximo mínimo. */\nexport function PixAutomaticoTerm({\n  label,\n  detail,\n  children,\n  className,\n  ...props\n}: PixAutomaticoTermProps) {\n  return (\n    <div data-slot=\"pix-automatico-term\" className={cn(\"contents\", className)} {...props}>\n      <dt className=\"text-subtle-foreground\">{label}</dt>\n      <dd className=\"m-0 min-w-0 text-right text-foreground tabular-nums\">\n        {children}\n        {detail ? (\n          <span className=\"block text-[12px]/4 text-subtle-foreground\">{detail}</span>\n        ) : null}\n      </dd>\n    </div>\n  );\n}\n\nconst NOTICE =\n  \"Você confirma no app do seu banco e pode definir um valor máximo por cobrança. Dá para cancelar quando quiser, direto no banco.\";\n\n/** The max-amount and cancel explanation. In compact it sits behind an info button. */\nexport function PixAutomaticoNotice({ className, children, ...props }: ComponentProps<\"div\">) {\n  const context = usePixAutomatico(\"PixAutomaticoNotice\");\n  const text = children ?? NOTICE;\n  if (context.variant === \"compact\") {\n    return (\n      <Popover>\n        <div\n          data-slot=\"pix-automatico-notice\"\n          className={cn(\"flex items-center gap-1.5\", className)}\n          {...props}\n        >\n          <span className=\"text-[12px]/4 text-subtle-foreground\">Como funciona o valor máximo</span>\n          <PopoverTrigger asChild>\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon\"\n              aria-label=\"Sobre o valor máximo e o cancelamento\"\n              className=\"size-6 rounded-md text-muted-foreground [transition:background-color_120ms_ease-out,color_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-muted hover:text-foreground active:scale-[0.94] motion-reduce:transition-none motion-reduce:active:scale-100\"\n            >\n              <Info className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />\n            </Button>\n          </PopoverTrigger>\n        </div>\n        <PopoverContent\n          side=\"top\"\n          sideOffset={6}\n          className=\"w-[260px] rounded-[14px] border-0 bg-popover p-3 text-[12px]/[17px] text-muted-foreground shadow-[0_0_0_1px_var(--border-strong),0_16px_32px_-12px_oklch(0_0_0/0.32),0_4px_8px_-4px_oklch(0_0_0/0.12)] data-[state=open]:zoom-in-96 data-open:zoom-in-96 data-[state=open]:duration-180 data-open:duration-180 data-[state=closed]:zoom-out-96 data-closed:zoom-out-96 motion-reduce:animate-none\"\n        >\n          {text}\n        </PopoverContent>\n      </Popover>\n    );\n  }\n  return (\n    <div\n      data-slot=\"pix-automatico-notice\"\n      className={cn(\n        \"flex min-w-0 items-start gap-2.5 rounded-[10px] bg-muted px-3 py-2.5 text-[12px]/[17px] text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      <Info className=\"mt-px size-3.5 shrink-0\" strokeWidth={1.75} aria-hidden=\"true\" />\n      <span>{text}</span>\n    </div>\n  );\n}\n\n/** The variable-amount hint: the bank only pays up to the payer's maximum. */\nexport function PixAutomaticoMaxHint({\n  minimum,\n  className,\n  children,\n  ...props\n}: ComponentProps<\"p\"> & { minimum?: number }) {\n  usePixAutomatico(\"PixAutomaticoMaxHint\");\n  return (\n    <p\n      data-slot=\"pix-automatico-max-hint\"\n      className={cn(\"m-0 text-[11.5px]/4 text-subtle-foreground\", className)}\n      {...props}\n    >\n      {children ??\n        (minimum === undefined\n          ? \"O banco só paga cobranças até o valor máximo que você definir no app.\"\n          : `O banco só paga cobranças até o valor máximo que você definir no app, e o recebedor pede que ele seja de pelo menos ${formatBrl(minimum)}.`)}\n    </p>\n  );\n}\n\nconst METHOD_LABELS: Record<PixAutomaticoMethod, { label: string; icon: typeof QrCode }> = {\n  qr: { label: \"QR Code\", icon: QrCode },\n  app: { label: \"No app do banco\", icon: Smartphone },\n};\n\n/**\n * \"Como você quer autorizar?\": QR Code or No app do banco, as a two-option radio group. A click,\n * tap, or Space on No app do banco opens the bank step; arrow keys only change the selection\n * (WCAG 3.2.2). Once a bank is chosen it shows under the segment with Trocar.\n */\nexport function PixAutomaticoMethod({\n  label = \"Como você quer autorizar?\",\n  className,\n  onKeyDown,\n  ...props\n}: Omit<ComponentProps<\"div\">, \"children\"> & { label?: string }) {\n  const context = usePixAutomatico(\"PixAutomaticoMethod\");\n  const refs = useRef<Record<string, HTMLButtonElement | null>>({});\n  const labelId = useId();\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event);\n    if (event.defaultPrevented) return;\n    const keys = [\"ArrowRight\", \"ArrowLeft\", \"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"];\n    if (!keys.includes(event.key)) return;\n    event.preventDefault();\n    const next: PixAutomaticoMethod =\n      event.key === \"Home\"\n        ? \"qr\"\n        : event.key === \"End\"\n          ? \"app\"\n          : context.method === \"qr\"\n            ? \"app\"\n            : \"qr\";\n    context.setMethod(next);\n    refs.current[next]?.focus();\n  };\n  return (\n    <div\n      data-slot=\"pix-automatico-method\"\n      className={cn(\"grid min-w-0 gap-2\", className)}\n      {...props}\n    >\n      <span id={labelId} className=\"text-[12px]/4 font-medium text-muted-foreground\">\n        {label}\n      </span>\n      <div\n        role=\"radiogroup\"\n        aria-labelledby={labelId}\n        onKeyDown={handleKeyDown}\n        className=\"relative isolate grid grid-cols-2 rounded-full bg-muted p-0.5\"\n      >\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"pointer-events-none absolute inset-y-0.5 left-0.5 -z-1 w-[calc(50%-2px)] rounded-full bg-card shadow-[0_0_0_1px_var(--border)] [transition:translate_240ms_cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none dark:bg-foreground/12 dark:shadow-none\",\n            context.method === \"app\" && \"translate-x-full\",\n          )}\n        />\n        {PIX_AUTOMATICO_METHODS.map((value) => {\n          const { label: text, icon: Icon } = METHOD_LABELS[value];\n          const checked = context.method === value;\n          return (\n            // biome-ignore lint/a11y/useSemanticElements: a segment button in a radio group; ToggleGroupItem renders the same role=\"radio\" button.\n            <button\n              key={value}\n              ref={(node) => {\n                refs.current[value] = node;\n              }}\n              type=\"button\"\n              role=\"radio\"\n              aria-checked={checked}\n              data-state={checked ? \"on\" : \"off\"}\n              tabIndex={checked ? 0 : -1}\n              disabled={context.status !== \"review\"}\n              onClick={(event) => {\n                context.setMethod(value);\n                if (value === \"app\") context.openBankPicker(event.currentTarget);\n              }}\n              className={cn(\n                toggleVariants(),\n                \"h-7 min-w-0 cursor-pointer gap-1.5 rounded-full border-0 bg-transparent px-3 text-[12px] font-medium text-subtle-foreground [transition:color_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-transparent hover:text-foreground focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.97] aria-checked:text-foreground data-[state=on]:bg-transparent motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg]:size-3.5\",\n              )}\n            >\n              <Icon aria-hidden=\"true\" strokeWidth={1.75} />\n              <span className=\"truncate\">{text}</span>\n            </button>\n          );\n        })}\n      </div>\n      {/* Added on the way back, not behind the open picker, so opening never changes the height. */}\n      {context.method === \"app\" && context.bank && context.step === \"terms\" ? (\n        <BankSummary bank={context.bank} />\n      ) : null}\n    </div>\n  );\n}\n\n/** The chosen bank on the terms step, with Trocar to reopen the picker. */\nfunction BankSummary({ bank }: { bank: PixAutomaticoBank }) {\n  const context = usePixAutomatico(\"PixAutomaticoMethod\");\n  const compact = context.variant === \"compact\";\n  return (\n    <div\n      data-slot=\"pix-automatico-bank-summary\"\n      className={cn(\n        \"flex min-w-0 items-center gap-2.5 rounded-[10px] pr-1.5 pl-2.5 shadow-[inset_0_0_0_1px_var(--border)]\",\n        // Fades up 4px as the terms come back; instant under reduced motion.\n        \"motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-1 motion-safe:duration-180 motion-safe:ease-out-quint\",\n        compact ? \"h-10 rounded-lg\" : \"h-11\",\n      )}\n    >\n      <PixAutomaticoBankMark bank={bank} />\n      <span className=\"min-w-0 flex-1 truncate text-[13px]/[18px] font-medium text-foreground\">\n        {bank.name}\n      </span>\n      <Button\n        type=\"button\"\n        variant=\"secondary\"\n        aria-label={`Trocar banco, ${bank.name} escolhido`}\n        disabled={context.status !== \"review\"}\n        onClick={(event) => context.openBankPicker(event.currentTarget)}\n        className={cn(pillClass, \"h-7 bg-secondary px-3 text-[12px] hover:bg-accent\")}\n      >\n        Trocar\n      </Button>\n    </div>\n  );\n}\n\nconst pillClass =\n  \"rounded-full border-0 font-medium [transition:background-color_120ms_ease-out,filter_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100\";\n\n/**\n * The one primary action: a full-width 36px pill. With No app do banco and no bank yet it reads\n * Escolher banco and opens the bank step; with a bank it sends the request right away.\n */\nexport function PixAutomaticoAuthorize({\n  className,\n  children,\n  onClick,\n  ...props\n}: ComponentProps<\"button\">) {\n  const context = usePixAutomatico(\"PixAutomaticoAuthorize\");\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"pix-automatico-authorize\"\n      className={cn(\n        pillClass,\n        \"h-9 w-full bg-primary text-[13px] text-primary-foreground hover:bg-primary hover:brightness-[1.08]\",\n        className,\n      )}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.authorize();\n      }}\n      {...props}\n    >\n      {children ??\n        (context.method === \"app\" && !context.bank ? \"Escolher banco\" : \"Autorizar Pix Automático\")}\n    </Button>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Bank picker: the bank step that swaps in for the terms\n * -----------------------------------------------------------------------------------------------*/\n\nconst TONE_CLASSES: Record<PixAutomaticoBankTone, string> = {\n  neutral: \"bg-muted text-muted-foreground shadow-[inset_0_0_0_1px_var(--border)]\",\n  primary: \"bg-[color-mix(in_oklab,var(--primary)_12%,var(--card))] text-primary-ink\",\n  success: \"bg-[color-mix(in_oklab,var(--success)_12%,var(--card))] text-success-ink\",\n  warning: \"bg-[color-mix(in_oklab,var(--warning)_12%,var(--card))] text-warning-ink\",\n};\n\nexport type PixAutomaticoBankMarkProps = Omit<ComponentProps<\"span\">, \"children\"> & {\n  bank: PixAutomaticoBank;\n  /** \"badge\" is the 24px tile on the waiting phone. Defaults to the variant's row size. */\n  size?: \"row\" | \"badge\";\n};\n\n/** The bank's tile: the consumer's logo, or its initials on a stable tone. Always decorative. */\nexport function PixAutomaticoBankMark({\n  bank,\n  size = \"row\",\n  className,\n  ...props\n}: PixAutomaticoBankMarkProps) {\n  const context = usePixAutomatico(\"PixAutomaticoBankMark\");\n  const [logoFailed, setLogoFailed] = useState(false);\n  const small = size === \"badge\" || context.variant === \"compact\";\n  const logo = bank.logoUrl && !logoFailed;\n  return (\n    <span\n      data-slot=\"pix-automatico-bank-mark\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"grid shrink-0 place-items-center overflow-hidden font-semibold tracking-[0.01em]\",\n        small ? \"size-6 rounded-[7px] text-[10px]/none\" : \"size-7 rounded-lg text-[11px]/none\",\n        logo\n          ? \"bg-white shadow-[inset_0_0_0_1px_var(--border)]\"\n          : TONE_CLASSES[pixAutomaticoBankTone(bank.id)],\n        className,\n      )}\n      {...props}\n    >\n      {logo ? (\n        // biome-ignore lint/performance/noImgElement: the logo comes from the consumer's assets\n        <img\n          src={bank.logoUrl}\n          alt=\"\"\n          loading=\"lazy\"\n          onError={() => setLogoFailed(true)}\n          className=\"size-full object-contain\"\n        />\n      ) : (\n        pixAutomaticoBankMonogram(bank)\n      )}\n    </span>\n  );\n}\n\nconst optionRow =\n  \"flex min-w-0 shrink-0 cursor-pointer items-center [transition:background-color_120ms_ease-out] select-none motion-reduce:transition-none\";\n\n/** \"Banrisul\" with the matched part in a <mark>. */\nfunction Highlight({ text, at, length }: { text: string; at: number; length: number }) {\n  const name = text.normalize(\"NFC\");\n  if (at < 0 || length === 0) return <>{name}</>;\n  return (\n    <>\n      {name.slice(0, at)}\n      <mark className=\"rounded-[3px] bg-[color-mix(in_oklab,var(--primary)_20%,var(--card))] text-inherit\">\n        {name.slice(at, at + length)}\n      </mark>\n      {name.slice(at + length)}\n    </>\n  );\n}\n\nfunction quoteQuery(query: string) {\n  const trimmed = query.trim();\n  return `“${trimmed.length > 32 ? `${trimmed.slice(0, 32)}…` : trimmed}”`;\n}\n\n/**\n * The bank step: a search combobox controlling a single-select listbox, the recent bank pinned on\n * top, and a footer that sends to the selected bank. Focus stays in the field while arrows move.\n */\nfunction PixAutomaticoBankPicker({ className, style }: { className?: string; style?: object }) {\n  const context = usePixAutomatico(\"PixAutomaticoBankPicker\");\n  const { banks, bank: selected, recentBank, banksStatus, pending } = context;\n  const compact = context.variant === \"compact\";\n  const baseId = useId();\n  const headingId = `${baseId}-heading`;\n  const listId = `${baseId}-list`;\n  const optionId = (bank: PixAutomaticoBank) => `${baseId}-${bank.id}`;\n  const [query, setQuery] = useState(\"\");\n  const [activeId, setActiveId] = useState<string | null>(selected?.id ?? null);\n  const [keyboard, setKeyboard] = useState(false);\n  const [overflowing, setOverflowing] = useState(false);\n  const heading = useRef<HTMLHeadingElement>(null);\n  const search = useRef<HTMLInputElement>(null);\n  const list = useRef<HTMLDivElement>(null);\n  const send = useRef<HTMLButtonElement>(null);\n  const typeahead = useRef({ buffer: \"\", at: 0 });\n  const searching = query.trim() !== \"\";\n  const ready = banksStatus === \"ready\";\n\n  const results = searching ? searchBanks(banks, query) : [];\n  const groups: { label: string; rows: { bank: PixAutomaticoBank; highlight: number }[] }[] =\n    searching\n      ? [{ label: \"Resultados\", rows: results }]\n      : [\n          ...(recentBank\n            ? [{ label: \"Usado da última vez\", rows: [{ bank: recentBank, highlight: -1 }] }]\n            : []),\n          {\n            label: \"Bancos\",\n            rows: banks\n              .filter((bank) => bank.id !== recentBank?.id)\n              .map((bank) => ({ bank, highlight: -1 })),\n          },\n        ];\n  const visible = groups.flatMap((group) => group.rows.map((row) => row.bank));\n  const active = visible.find((bank) => bank.id === activeId) ?? null;\n\n  // Fine pointers land in the search field; coarse pointers on the heading, so the keyboard\n  // doesn't cover the list. The same happens when a retried list becomes ready, so focus isn't\n  // lost with the error state's buttons.\n  const landed = useRef<PixAutomaticoBanksStatus | null>(null);\n  useEffect(() => {\n    const previous = landed.current;\n    landed.current = banksStatus;\n    if (previous !== null && (previous === \"ready\" || banksStatus !== \"ready\")) return;\n    const coarse = window.matchMedia?.(\"(pointer: coarse)\").matches;\n    if (!coarse && search.current && !search.current.disabled) search.current.focus();\n    else heading.current?.focus();\n  }, [banksStatus]);\n\n  // The result count, 400ms after the last keystroke and only while searching.\n  const { announce } = context;\n  useEffect(() => {\n    if (!searching) {\n      announce(\"\");\n      return;\n    }\n    const count = results.length;\n    const timer = window.setTimeout(\n      () =>\n        announce(\n          count === 0\n            ? \"Nenhum banco encontrado\"\n            : count === 1\n              ? \"1 banco encontrado\"\n              : `${count} bancos encontrados`,\n        ),\n      400,\n    );\n    return () => window.clearTimeout(timer);\n  }, [searching, results.length, announce]);\n\n  useEffect(() => {\n    if (banksStatus === \"loading\") announce(\"Carregando bancos\");\n    if (banksStatus === \"error\") announce(\"Não deu para carregar a lista de bancos\");\n  }, [banksStatus, announce]);\n\n  // The fade mask only shows when the list overflows: re-checked after each render (filtering)\n  // and when the step resizes.\n  const measureOverflow = () => {\n    const node = list.current;\n    if (node) setOverflowing(node.scrollHeight > node.clientHeight + 1);\n  };\n  useLayoutEffect(measureOverflow);\n  useEffect(() => {\n    const node = list.current;\n    if (!node || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      setOverflowing(node.scrollHeight > node.clientHeight + 1);\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  useEffect(() => {\n    if (!activeId) return;\n    document.getElementById(`${baseId}-${activeId}`)?.scrollIntoView?.({ block: \"nearest\" });\n  }, [activeId, baseId]);\n\n  const activate = (bank: PixAutomaticoBank | undefined) => {\n    if (!bank) return;\n    setKeyboard(true);\n    setActiveId(bank.id);\n    // Selection follows focus; disabled rows are reachable so their reason is read.\n    context.selectBank(bank);\n  };\n\n  const move = (delta: 1 | -1) => {\n    const index = active ? visible.indexOf(active) : -1;\n    if (index === -1) {\n      const enabled = visible.filter((bank) => !bank.disabled);\n      activate(delta === 1 ? enabled[0] : enabled[enabled.length - 1]);\n      return;\n    }\n    activate(visible[Math.min(visible.length - 1, Math.max(0, index + delta))]);\n  };\n\n  const commit = () => {\n    if (active) context.selectBank(active);\n    send.current?.focus();\n  };\n\n  const clear = () => {\n    setQuery(\"\");\n    search.current?.focus();\n  };\n\n  const useQr = () => {\n    context.setMethod(\"qr\");\n    context.closeBankPicker();\n  };\n\n  const onSearchKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n      event.preventDefault();\n      move(event.key === \"ArrowDown\" ? 1 : -1);\n    } else if (event.key === \"Enter\") {\n      event.preventDefault();\n      if (active) commit();\n    } else if (event.key === \"Escape\") {\n      event.preventDefault();\n      if (query) setQuery(\"\");\n      else context.closeBankPicker();\n    }\n  };\n\n  const onListKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    const enabled = visible.filter((bank) => !bank.disabled);\n    if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n      event.preventDefault();\n      move(event.key === \"ArrowDown\" ? 1 : -1);\n    } else if (event.key === \"Home\" || event.key === \"End\") {\n      event.preventDefault();\n      activate(event.key === \"Home\" ? enabled[0] : enabled[enabled.length - 1]);\n    } else if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      commit();\n    } else if (event.key === \"Escape\") {\n      event.preventDefault();\n      context.closeBankPicker();\n    } else if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {\n      // Type-ahead: a 500ms buffer; repeating one letter cycles through its matches.\n      const now = Date.now();\n      const state = typeahead.current;\n      state.buffer = now - state.at > 500 ? event.key : state.buffer + event.key;\n      state.at = now;\n      const needle = fold(state.buffer);\n      const cycling = needle.length > 1 && new Set(needle).size === 1;\n      const prefix = cycling ? (needle[0] ?? \"\") : needle;\n      const current = active ? enabled.indexOf(active) : -1;\n      const start = current === -1 ? 0 : current + (needle.length === 1 || cycling ? 1 : 0);\n      const ordered = [...enabled.slice(start), ...enabled.slice(0, start)];\n      activate(ordered.find((bank) => fold(bank.name).startsWith(prefix)));\n    }\n  };\n\n  const sendLabel = pending\n    ? \"Enviando…\"\n    : selected\n      ? `Enviar para o app ${bankShortName(selected)}`\n      : \"Escolha um banco\";\n  const canSend = Boolean(selected) && !pending && ready;\n  let position = 0;\n\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: the step is a labelled group inside the card, not a fieldset of form inputs.\n    <div\n      role=\"group\"\n      aria-labelledby={headingId}\n      aria-busy={pending || undefined}\n      data-slot=\"pix-automatico-bank-picker\"\n      className={cn(\n        \"grid min-h-0 min-w-0 grid-rows-[auto_auto_minmax(0,1fr)_auto] [grid-area:1/1]\",\n        compact ? \"gap-2.5\" : \"gap-3\",\n        \"motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-right-2 motion-safe:duration-240 motion-safe:delay-[60ms] motion-safe:ease-out-quint motion-safe:fill-mode-backwards\",\n        className,\n      )}\n      style={style}\n      onPointerDown={() => setKeyboard(false)}\n    >\n      <div className=\"flex min-w-0 items-center gap-2\">\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          aria-label=\"Voltar para os termos\"\n          onClick={context.closeBankPicker}\n          className={cn(\n            \"shrink-0 rounded-md text-muted-foreground [transition:background-color_120ms_ease-out,color_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-muted hover:text-foreground active:scale-[0.94] motion-reduce:transition-none motion-reduce:active:scale-100\",\n            compact ? \"size-6\" : \"size-7\",\n          )}\n        >\n          <ChevronLeft className=\"size-4\" strokeWidth={1.75} aria-hidden=\"true\" />\n        </Button>\n        <div className=\"grid min-w-0\">\n          <h4\n            ref={heading}\n            id={headingId}\n            tabIndex={-1}\n            className=\"m-0 truncate text-[13px]/[18px] font-medium text-foreground outline-none\"\n          >\n            Escolha seu banco\n          </h4>\n          {!compact ? (\n            <p className=\"m-0 truncate text-[12px]/4 text-subtle-foreground\">\n              O pedido vai para o app que você escolher.\n            </p>\n          ) : null}\n        </div>\n      </div>\n\n      {banksStatus === \"error\" ? (\n        <span />\n      ) : (\n        <div\n          className={cn(\n            \"flex min-w-0 items-center gap-2 bg-background pr-1.5 pl-[11px] shadow-[inset_0_0_0_1px_var(--border)] [transition:box-shadow_120ms_ease-out] focus-within:shadow-[inset_0_0_0_1px_var(--ring),0_0_0_3px_color-mix(in_oklab,var(--ring)_30%,transparent)] motion-reduce:transition-none\",\n            compact ? \"h-8 rounded-lg\" : \"h-9 rounded-[10px]\",\n            !ready && \"opacity-55\",\n          )}\n        >\n          <label htmlFor={`${baseId}-search`} className=\"sr-only\">\n            Buscar banco\n          </label>\n          <Search\n            className=\"size-3.5 shrink-0 text-subtle-foreground\"\n            strokeWidth={1.75}\n            aria-hidden=\"true\"\n          />\n          <Input\n            ref={search}\n            id={`${baseId}-search`}\n            type=\"search\"\n            role=\"combobox\"\n            aria-expanded=\"true\"\n            aria-controls={listId}\n            aria-autocomplete=\"list\"\n            aria-activedescendant={active ? optionId(active) : undefined}\n            autoComplete=\"off\"\n            enterKeyHint=\"search\"\n            spellCheck={false}\n            placeholder=\"Buscar banco\"\n            disabled={!ready}\n            value={query}\n            onChange={(event) => setQuery(event.target.value)}\n            onKeyDown={onSearchKeyDown}\n            className=\"peer h-auto min-w-0 flex-1 rounded-none border-0 bg-transparent p-0 text-[13px] text-foreground shadow-none outline-none placeholder:text-subtle-foreground focus-visible:border-0 focus-visible:ring-0 disabled:opacity-100 md:text-[13px] dark:bg-transparent [&::-webkit-search-cancel-button]:hidden\"\n          />\n          {query ? (\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon\"\n              aria-label=\"Limpar busca\"\n              onClick={clear}\n              className=\"size-6 shrink-0 rounded-md text-subtle-foreground [transition:background-color_120ms_ease-out,color_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-muted hover:text-foreground active:scale-[0.94] motion-reduce:transition-none motion-reduce:active:scale-100\"\n            >\n              <X className=\"size-3.5\" strokeWidth={2} aria-hidden=\"true\" />\n            </Button>\n          ) : null}\n        </div>\n      )}\n\n      <div\n        ref={list}\n        id={listId}\n        role=\"listbox\"\n        aria-label=\"Bancos\"\n        aria-busy={banksStatus === \"loading\" || undefined}\n        aria-activedescendant={active ? optionId(active) : undefined}\n        tabIndex={ready && visible.length > 0 ? 0 : -1}\n        onKeyDown={onListKeyDown}\n        onFocus={() => {\n          if (!active && visible.length > 0) setActiveId(selected?.id ?? visible[0]?.id ?? null);\n        }}\n        className={cn(\n          \"grid min-h-0 min-w-0 content-start overflow-y-auto overscroll-contain outline-none\",\n          overflowing && \"[mask-image:linear-gradient(black_calc(100%-28px),transparent)]\",\n          (banksStatus !== \"ready\" || (searching && results.length === 0)) && \"content-center\",\n        )}\n      >\n        {banksStatus === \"loading\" ? (\n          <div className=\"grid gap-1\">\n            {Array.from({ length: compact ? 3 : 4 }, (_, index) => (\n              <div\n                // biome-ignore lint/suspicious/noArrayIndexKey: fixed skeleton rows.\n                key={index}\n                aria-hidden=\"true\"\n                className={cn(\"flex items-center gap-2.5 px-2.5\", compact ? \"h-10\" : \"h-11\")}\n              >\n                <span className=\"size-7 shrink-0 rounded-lg bg-muted\" />\n                <span className=\"h-3 w-28 rounded-[4px] bg-[linear-gradient(90deg,var(--muted)_0%,color-mix(in_oklab,var(--muted)_55%,var(--card))_50%,var(--muted)_100%)] bg-size-[200%_100%] motion-safe:animate-skeleton-shimmer\" />\n              </div>\n            ))}\n          </div>\n        ) : banksStatus === \"error\" ? (\n          <div className=\"grid justify-items-center gap-1 px-3 text-center motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-1 motion-safe:duration-180\">\n            <p className=\"m-0 text-[12.5px]/[18px] font-medium text-destructive-ink\">\n              Não deu para carregar a lista de bancos.\n            </p>\n            <p className=\"m-0 text-[12.5px]/[18px] text-muted-foreground\">\n              Tente de novo ou use o QR Code.\n            </p>\n            <div className=\"mt-2 flex flex-wrap justify-center gap-2\">\n              <Button\n                type=\"button\"\n                variant=\"secondary\"\n                onClick={useQr}\n                className={cn(\n                  pillClass,\n                  \"h-7 gap-1.5 bg-secondary px-3 text-[12px] hover:bg-accent has-[>svg]:px-3\",\n                )}\n              >\n                <QrCode className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />\n                Usar QR Code\n              </Button>\n              {context.onBanksRetry ? (\n                <Button\n                  type=\"button\"\n                  variant=\"secondary\"\n                  onClick={context.onBanksRetry}\n                  className={cn(pillClass, \"h-7 bg-secondary px-3 text-[12px] hover:bg-accent\")}\n                >\n                  Tentar de novo\n                </Button>\n              ) : null}\n            </div>\n          </div>\n        ) : searching && results.length === 0 ? (\n          <div className=\"grid justify-items-center gap-2 px-3 text-center motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-1 motion-safe:duration-180\">\n            <span\n              aria-hidden=\"true\"\n              className=\"mb-1 grid size-10 place-items-center rounded-full bg-muted text-muted-foreground\"\n            >\n              <SearchX className=\"size-4\" strokeWidth={1.75} />\n            </span>\n            <p className=\"m-0 text-[13px]/[18px] font-medium text-foreground\">\n              Nenhum banco encontrado para {quoteQuery(query)}\n            </p>\n            <p className=\"m-0 max-w-[280px] text-[12.5px]/[18px] text-muted-foreground\">\n              Confira o nome ou use o QR Code, que funciona com qualquer banco.\n            </p>\n            <div className=\"mt-1 flex flex-wrap justify-center gap-2\">\n              <Button\n                type=\"button\"\n                variant=\"secondary\"\n                onClick={clear}\n                className={cn(pillClass, \"h-7 bg-secondary px-3 text-[12px] hover:bg-accent\")}\n              >\n                Limpar busca\n              </Button>\n              <Button\n                type=\"button\"\n                variant=\"secondary\"\n                onClick={useQr}\n                className={cn(\n                  pillClass,\n                  \"h-7 gap-1.5 bg-secondary px-3 text-[12px] hover:bg-accent has-[>svg]:px-3\",\n                )}\n              >\n                <QrCode className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />\n                Usar QR Code\n              </Button>\n            </div>\n          </div>\n        ) : (\n          groups.map((group) => {\n            const groupId = `${baseId}-group-${group.label.length}`;\n            return (\n              // biome-ignore lint/a11y/useSemanticElements: an ARIA group of options inside a listbox; a fieldset can't own options.\n              <div\n                key={group.label}\n                role=\"group\"\n                aria-labelledby={groupId}\n                className=\"grid min-w-0\"\n              >\n                <span\n                  id={groupId}\n                  className=\"px-2.5 pt-1.5 pb-1 text-[11.5px]/4 font-medium text-subtle-foreground first:pt-0\"\n                >\n                  {group.label}\n                </span>\n                {group.rows.map(({ bank, highlight }) => {\n                  position += 1;\n                  const isSelected = bank.id === selected?.id;\n                  const isActive = keyboard && bank.id === active?.id;\n                  const reasonId = `${optionId(bank)}-reason`;\n                  return (\n                    // biome-ignore lint/a11y/useKeyWithClickEvents: keys go to the search field or the listbox, which own aria-activedescendant.\n                    <div\n                      key={bank.id}\n                      id={optionId(bank)}\n                      role=\"option\"\n                      aria-selected={isSelected}\n                      aria-disabled={bank.disabled || undefined}\n                      aria-describedby={bank.disabled ? reasonId : undefined}\n                      aria-setsize={visible.length}\n                      aria-posinset={position}\n                      tabIndex={-1}\n                      data-active={isActive || undefined}\n                      onClick={() => {\n                        setKeyboard(false);\n                        setActiveId(bank.id);\n                        context.selectBank(bank);\n                      }}\n                      className={cn(\n                        optionRow,\n                        compact\n                          ? \"h-10 gap-[9px] rounded-lg px-2 pointer-coarse:h-11\"\n                          : \"h-11 gap-2.5 rounded-[10px] px-2.5\",\n                        \"data-active:outline-2 data-active:-outline-offset-2 data-active:outline-primary data-active:outline-solid\",\n                        bank.disabled\n                          ? \"cursor-default\"\n                          : isSelected\n                            ? \"bg-[color-mix(in_oklab,var(--primary)_6%,var(--card))]\"\n                            : \"hover:bg-muted\",\n                      )}\n                    >\n                      <PixAutomaticoBankMark\n                        bank={bank}\n                        className={cn(bank.disabled && \"opacity-60 grayscale\")}\n                      />\n                      <span\n                        className={cn(\n                          \"min-w-0 flex-1 truncate text-[13px]/[18px]\",\n                          bank.disabled\n                            ? \"font-normal text-subtle-foreground\"\n                            : \"font-medium text-foreground\",\n                        )}\n                      >\n                        <Highlight\n                          text={bank.name}\n                          at={highlight}\n                          length={fold(query.trim()).length}\n                        />\n                      </span>\n                      {bank.disabled ? (\n                        // Read through aria-describedby, so the option's name stays the bank name.\n                        <span\n                          id={reasonId}\n                          aria-hidden=\"true\"\n                          className=\"shrink-0 text-[11.5px]/4 text-subtle-foreground\"\n                        >\n                          {bank.disabledReason ?? \"Indisponível\"}\n                        </span>\n                      ) : (\n                        <span\n                          aria-hidden=\"true\"\n                          className={cn(\n                            \"grid size-4 shrink-0 place-items-center rounded-full\",\n                            isSelected\n                              ? \"bg-primary text-primary-foreground motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-50 motion-safe:duration-180 motion-safe:ease-[cubic-bezier(0.16,1,0.3,1)]\"\n                              : \"shadow-[inset_0_0_0_1.5px_var(--subtle-foreground)]\",\n                          )}\n                        >\n                          {isSelected ? <Check className=\"size-3\" strokeWidth={2.6} /> : null}\n                        </span>\n                      )}\n                    </div>\n                  );\n                })}\n              </div>\n            );\n          })\n        )}\n      </div>\n\n      <div className=\"grid min-w-0 gap-2.5\">\n        <div className=\"flex min-w-0 items-center justify-between gap-2 text-[12px]/4\">\n          <span className=\"text-subtle-foreground\">Não achou seu banco?</span>\n          <button\n            type=\"button\"\n            onClick={useQr}\n            className=\"cursor-pointer rounded-sm font-medium text-primary-ink [transition:opacity_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100\"\n          >\n            Usar QR Code\n          </button>\n        </div>\n        <Button\n          ref={send}\n          type=\"button\"\n          data-slot=\"pix-automatico-bank-send\"\n          aria-disabled={!canSend || undefined}\n          onClick={() => {\n            if (canSend) context.sendToBank();\n          }}\n          className={cn(\n            pillClass,\n            \"h-9 w-full gap-2 bg-primary text-[13px] text-primary-foreground hover:bg-primary\",\n            canSend ? \"hover:brightness-[1.08]\" : \"cursor-default\",\n            !canSend && !pending && \"opacity-45\",\n          )}\n        >\n          {pending ? (\n            <LoaderCircle\n              className=\"size-3.5 motion-safe:animate-spin\"\n              strokeWidth={2}\n              aria-hidden=\"true\"\n            />\n          ) : null}\n          <span className=\"truncate\">{sendLabel}</span>\n        </Button>\n      </div>\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Awaiting\n * -----------------------------------------------------------------------------------------------*/\n\nconst enterPanel =\n  \"grid min-w-0 animate-in gap-4 delay-120 duration-240 ease-out-quint fill-mode-backwards fade-in-0 slide-in-from-bottom-1 motion-reduce:animate-none\";\n\nconst QR_STEPS = [\n  \"Abra o app do seu banco e escolha ler QR Code.\",\n  \"Confira os dados e defina um valor máximo, se quiser.\",\n  \"Confirme. Avisamos aqui na hora.\",\n];\n\nexport type PixAutomaticoQrCodeProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  /** The QR image from your PSP. Use `children` instead for an inline SVG. */\n  src?: string;\n  alt?: string;\n  children?: ReactNode;\n  steps?: readonly ReactNode[];\n};\n\n/** Awaiting with QR Code: the white tile, three steps, the countdown, and Copiar código. */\nexport function PixAutomaticoQrCode({\n  src,\n  alt = \"QR Code do Pix Automático\",\n  steps = QR_STEPS,\n  children,\n  className,\n  ...props\n}: PixAutomaticoQrCodeProps) {\n  const context = usePixAutomatico(\"PixAutomaticoQrCode\");\n  if (context.status !== \"awaiting\" || context.method !== \"qr\") return null;\n  const compact = context.variant === \"compact\";\n  const urgent = context.secondsLeft !== null && context.secondsLeft <= 60;\n  return (\n    <div\n      data-slot=\"pix-automatico-qr-code\"\n      className={cn(enterPanel, compact && \"gap-3\", className)}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"grid min-w-0 items-center gap-4\",\n          compact\n            ? \"grid-cols-1 justify-items-center\"\n            : \"@min-[320px]/pa:grid-cols-[136px_minmax(0,1fr)]\",\n        )}\n      >\n        <figure\n          id={context.waitingTitleId}\n          tabIndex={-1}\n          className={cn(\n            \"m-0 grid size-[136px] outline-none animate-in place-items-center justify-self-center rounded-xl bg-white p-2.5 shadow-[0_0_0_1px_var(--border)] delay-120 duration-180 ease-[cubic-bezier(0.16,1,0.3,1)] fill-mode-backwards zoom-in-96 motion-reduce:animate-none [&_img]:size-full [&_svg]:size-full\",\n            compact && \"size-28 p-2\",\n          )}\n        >\n          {/* biome-ignore lint/performance/noImgElement: the QR comes from the payment provider */}\n          {children ?? (src ? <img src={src} alt={alt} /> : null)}\n        </figure>\n        {!compact ? (\n          <ol className=\"m-0 grid list-none gap-2 p-0 text-[12.5px]/[18px] text-muted-foreground [counter-reset:pa-step]\">\n            {steps.map((step, index) => (\n              <li\n                // biome-ignore lint/suspicious/noArrayIndexKey: the steps are a fixed, ordered list.\n                key={index}\n                className=\"flex gap-2.5 [counter-increment:pa-step] before:grid before:size-[18px] before:shrink-0 before:place-items-center before:rounded-full before:bg-muted before:text-[10.5px] before:font-medium before:text-muted-foreground before:tabular-nums before:content-[counter(pa-step)]\"\n              >\n                {step}\n              </li>\n            ))}\n          </ol>\n        ) : null}\n      </div>\n      <div className=\"flex min-w-0 flex-wrap items-center justify-between gap-2\">\n        {context.secondsLeft !== null ? (\n          <p\n            className={cn(\n              \"m-0 text-[12px]/4 text-subtle-foreground [transition:color_240ms_ease-out] motion-reduce:transition-none\",\n              urgent && \"text-warning-ink\",\n            )}\n          >\n            Expira em{\" \"}\n            <time\n              dateTime={`PT${context.secondsLeft}S`}\n              className={cn(\n                \"font-medium tabular-nums\",\n                urgent ? \"text-warning-ink\" : \"text-foreground\",\n              )}\n            >\n              <NumberTick value={formatClock(context.secondsLeft)} />\n            </time>\n          </p>\n        ) : (\n          <span />\n        )}\n        {context.code ? (\n          <Button\n            type=\"button\"\n            variant=\"secondary\"\n            data-slot=\"pix-automatico-copy\"\n            onClick={() => void context.copy()}\n            className={cn(\n              pillClass,\n              \"h-7 gap-1.5 px-3 text-[12px] has-[>svg]:px-3\",\n              context.copied\n                ? \"bg-success/14 text-success-ink hover:bg-success/14\"\n                : \"bg-secondary hover:bg-accent\",\n            )}\n          >\n            {context.copied ? (\n              <Check\n                key=\"copied\"\n                className=\"size-3.5 animate-in duration-200 ease-out-quint fade-in-0 zoom-in-50 motion-reduce:animate-none\"\n                strokeWidth={2.2}\n                aria-hidden=\"true\"\n              />\n            ) : (\n              <Copy key=\"copy\" className=\"size-3.5\" strokeWidth={2} aria-hidden=\"true\" />\n            )}\n            <span className=\"grid *:[grid-area:1/1]\">\n              <span className={cn(context.copied && \"invisible\")} aria-hidden={context.copied}>\n                Copiar código\n              </span>\n              <span className={cn(!context.copied && \"invisible\")} aria-hidden={!context.copied}>\n                Copiado\n              </span>\n            </span>\n          </Button>\n        ) : null}\n      </div>\n      {context.copyFailed ? (\n        <p className=\"m-0 text-[11.5px]/4 text-subtle-foreground\">\n          Não deu para copiar. Leia o QR Code no app do banco.\n        </p>\n      ) : null}\n      <span role=\"status\" className=\"sr-only\">\n        {context.copied ? \"Código copiado\" : \"\"}\n      </span>\n    </div>\n  );\n}\n\n/**\n * Awaiting in the bank app: a pulsing phone tile badged with the bank, a shimmering title that\n * names it, the QR fallback, and Trocar banco when the consumer passes onChangeBank.\n */\nexport function PixAutomaticoAppPending({\n  className,\n  ...props\n}: Omit<ComponentProps<\"div\">, \"children\">) {\n  const context = usePixAutomatico(\"PixAutomaticoAppPending\");\n  if (context.status !== \"awaiting\" || context.method !== \"app\") return null;\n  const compact = context.variant === \"compact\";\n  const { bank } = context;\n  const app = bank ? bankShortName(bank) : null;\n  return (\n    <div\n      data-slot=\"pix-automatico-app-pending\"\n      className={cn(enterPanel, compact && \"gap-3\", className)}\n      {...props}\n    >\n      <div className=\"flex min-w-0 items-center gap-4\">\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"relative grid shrink-0 place-items-center rounded-2xl bg-primary/14 text-primary-ink\",\n            compact ? \"size-11\" : \"size-[52px]\",\n          )}\n        >\n          <span className=\"pointer-events-none absolute -inset-1.5 rounded-[20px] shadow-[0_0_0_1.5px_color-mix(in_oklab,var(--primary)_35%,transparent)] motion-safe:animate-ring-pulse\" />\n          <Smartphone className=\"size-5\" strokeWidth={1.75} />\n          {bank ? (\n            <PixAutomaticoBankMark\n              bank={bank}\n              size=\"badge\"\n              className=\"absolute -right-2 -bottom-2 z-1 shadow-[0_0_0_2px_var(--card)] motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-50 motion-safe:duration-180 motion-safe:delay-160 motion-safe:ease-[cubic-bezier(0.16,1,0.3,1)] motion-safe:fill-mode-backwards\"\n            />\n          ) : null}\n        </span>\n        <div className=\"grid min-w-0 gap-1\">\n          <h4\n            id={context.waitingTitleId}\n            tabIndex={-1}\n            className=\"m-0 text-[13px]/[18px] font-medium text-foreground outline-none\"\n          >\n            <span className=\"motion-safe:shimmer-text\">\n              {app ? `Aguardando confirmação no app ${app}` : \"Aguardando confirmação no banco\"}\n            </span>\n          </h4>\n          <p className=\"m-0 text-[12.5px]/[18px] text-muted-foreground\">\n            {app\n              ? `Abra o app ${app} e confirme o Pix Automático. O pedido pode levar alguns minutos para aparecer.`\n              : \"Enviamos o pedido para o app do seu banco. Pode levar alguns minutos para aparecer.\"}\n          </p>\n        </div>\n      </div>\n      <div className=\"flex min-w-0 flex-wrap items-center justify-between gap-2\">\n        <span className=\"text-[11.5px]/4 text-subtle-foreground\">Não apareceu?</span>\n        <div className=\"flex flex-wrap justify-end gap-2\">\n          {context.changeBank ? (\n            <Button\n              type=\"button\"\n              variant=\"secondary\"\n              onClick={context.changeBank}\n              className={cn(pillClass, \"h-7 bg-secondary px-3 text-[12px] hover:bg-accent\")}\n            >\n              Trocar banco\n            </Button>\n          ) : null}\n          <Button\n            type=\"button\"\n            variant=\"secondary\"\n            onClick={() => context.setMethod(\"qr\")}\n            className={cn(\n              pillClass,\n              \"h-7 gap-1.5 bg-secondary px-3 text-[12px] hover:bg-accent has-[>svg]:px-3\",\n            )}\n          >\n            <QrCode className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />\n            Usar QR Code\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Active\n * -----------------------------------------------------------------------------------------------*/\n\n/** The success tile, \"Pix Automático autorizado\", and the consumer's summary line. */\nexport function PixAutomaticoActive({ className, children, ...props }: ComponentProps<\"div\">) {\n  const context = usePixAutomatico(\"PixAutomaticoActive\");\n  if (context.status !== \"active\") return null;\n  return (\n    <div\n      data-slot=\"pix-automatico-active\"\n      className={cn(\n        \"flex min-w-0 animate-in items-center gap-3 duration-160 fade-in-0 motion-reduce:animate-none\",\n        className,\n      )}\n      {...props}\n    >\n      <span\n        aria-hidden=\"true\"\n        className=\"grid size-10 shrink-0 animate-in place-items-center rounded-full bg-success/14 text-success-ink duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] zoom-in-50 motion-reduce:animate-none\"\n      >\n        <Check className=\"size-5\" strokeWidth={2} />\n      </span>\n      <div className=\"grid min-w-0\">\n        <p className=\"m-0 text-[14px]/5 font-semibold text-foreground\">Pix Automático autorizado</p>\n        {children ? (\n          <p className=\"m-0 text-[12.5px]/[18px] text-muted-foreground tabular-nums\">{children}</p>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\nconst CHIP_DELAYS = [\n  \"delay-0\",\n  \"delay-[40ms]\",\n  \"delay-[80ms]\",\n  \"delay-[120ms]\",\n  \"delay-[160ms]\",\n  \"delay-[200ms]\",\n];\n\nexport type PixAutomaticoScheduleProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  charges: readonly PixAutomaticoCharge[];\n  /** The change-or-cancel line under the timeline. Pass `null` to hide it. */\n  hint?: ReactNode;\n};\n\n/** The next charges as a horizontal timeline; the first is marked Próxima. */\nexport function PixAutomaticoSchedule({\n  charges,\n  hint = \"Para mudar o valor máximo ou cancelar, use o app do seu banco.\",\n  className,\n  ...props\n}: PixAutomaticoScheduleProps) {\n  const context = usePixAutomatico(\"PixAutomaticoSchedule\");\n  const labelId = useId();\n  if (context.status !== \"active\") return null;\n  const shown = charges.slice(0, 6);\n  return (\n    <div\n      data-slot=\"pix-automatico-schedule\"\n      className={cn(\"grid min-w-0 gap-2.5\", className)}\n      {...props}\n    >\n      <span id={labelId} className=\"text-[12px]/4 font-medium text-muted-foreground\">\n        Próximas cobranças\n      </span>\n      <ol\n        aria-labelledby={labelId}\n        className=\"relative m-0 grid list-none gap-2 p-0 before:absolute before:top-[7px] before:right-3 before:left-3 before:h-px before:bg-border\"\n        style={{ gridTemplateColumns: `repeat(${Math.max(1, shown.length)}, minmax(0, 1fr))` }}\n      >\n        {shown.map((charge, index) => {\n          const { day, month } = calendarDay(charge.date);\n          const next = index === 0;\n          const amount = formatBrl(charge.amount);\n          return (\n            <li\n              key={String(charge.date)}\n              className={cn(\n                \"relative grid min-w-0 animate-in content-start gap-1.5 duration-240 ease-out-quint fill-mode-backwards fade-in-0 slide-in-from-bottom-1 motion-reduce:animate-none\",\n                CHIP_DELAYS[index],\n              )}\n            >\n              <span className=\"sr-only\">\n                {`${day} de ${MONTHS_LONG[month]}, ${amount}${next ? \", próxima\" : \"\"}`}\n              </span>\n              <span\n                aria-hidden=\"true\"\n                className={cn(\n                  \"relative grid size-[15px] place-items-center rounded-full\",\n                  next ? \"bg-primary\" : \"bg-card shadow-[inset_0_0_0_1.5px_var(--border-strong)]\",\n                )}\n              >\n                {next ? (\n                  <span className=\"size-1.5 rounded-full bg-primary-foreground motion-safe:animate-in motion-safe:zoom-in-0 motion-safe:duration-150 motion-safe:delay-120 motion-safe:fill-mode-backwards\" />\n                ) : null}\n              </span>\n              <span\n                aria-hidden=\"true\"\n                className=\"text-[12.5px]/4 font-medium text-foreground tabular-nums\"\n              >\n                {String(day).padStart(2, \"0\")} {MONTHS_SHORT[month]}\n              </span>\n              <span\n                aria-hidden=\"true\"\n                className=\"text-[12px]/4 text-subtle-foreground tabular-nums\"\n              >\n                {amount}\n              </span>\n              {next ? (\n                <span aria-hidden=\"true\" className=\"text-[11px]/4 font-medium text-primary-ink\">\n                  Próxima\n                </span>\n              ) : null}\n            </li>\n          );\n        })}\n      </ol>\n      {hint ? <p className=\"m-0 text-[11.5px]/4 text-subtle-foreground\">{hint}</p> : null}\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Outcomes\n * -----------------------------------------------------------------------------------------------*/\n\n/** Declined, expired, or cancelled: one sentence and the way forward. */\nexport function PixAutomaticoOutcome({ className, children, ...props }: ComponentProps<\"div\">) {\n  const context = usePixAutomatico(\"PixAutomaticoOutcome\");\n  const { status } = context;\n  if (status !== \"declined\" && status !== \"expired\" && status !== \"cancelled\") return null;\n  const compact = context.variant === \"compact\";\n  const size = compact ? \"h-7 px-3 text-[12px]\" : \"h-8 px-3.5 text-[12.5px]\";\n  const message =\n    status === \"declined\"\n      ? \"A autorização foi recusada no app do banco. Nenhuma cobrança foi feita.\"\n      : status === \"expired\"\n        ? \"O pedido expirou antes da confirmação.\"\n        : \"Você cancelou o Pix Automático no seu banco. Nenhuma nova cobrança será feita.\";\n  return (\n    <div\n      data-slot=\"pix-automatico-outcome\"\n      className={cn(\n        \"grid min-w-0 animate-in gap-3 duration-240 ease-out-quint fade-in-0 motion-reduce:animate-none\",\n        className,\n      )}\n      {...props}\n    >\n      <p className=\"m-0 text-[12.5px]/[18px] text-muted-foreground\">{children ?? message}</p>\n      {status !== \"cancelled\" && (context.onRetry || context.onOtherMethod) ? (\n        <div className=\"flex flex-wrap justify-end gap-2\">\n          {status === \"declined\" && context.onOtherMethod ? (\n            <Button\n              type=\"button\"\n              variant=\"secondary\"\n              onClick={context.onOtherMethod}\n              className={cn(pillClass, size, \"bg-secondary hover:bg-accent\")}\n            >\n              Outra forma de pagamento\n            </Button>\n          ) : null}\n          {context.onRetry ? (\n            <Button\n              type=\"button\"\n              onClick={context.onRetry}\n              className={cn(\n                pillClass,\n                size,\n                \"bg-primary text-primary-foreground hover:bg-primary hover:brightness-[1.08]\",\n              )}\n            >\n              {status === \"declined\" ? \"Tentar de novo\" : \"Gerar novo pedido\"}\n            </Button>\n          ) : null}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/pix-automatico.tsx"
    }
  ],
  "categories": [
    "brazil",
    "commerce",
    "checkout"
  ],
  "type": "registry:ui"
}