{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "store-status",
  "title": "Store Status",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Aberto/fechado calculado no fuso da loja, não no do visitante. Quem está do outro lado do mundo vê a resposta certa.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/visant/ui/store-status.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * StoreStatus — \"aberto agora\" de loja física.\n *\n * Colhido de um concept de e-commerce, e a razão dele existir é uma regra de\n * honestidade, não de UI:\n *\n * **O visitante pode estar em qualquer fuso.** Se você resolver a hora no\n * relógio do navegador, um cliente em Lisboa vê \"aberto\" com a loja de Londres\n * fechada. A hora é sempre resolvida no fuso DA LOJA, via `Intl`.\n *\n * E o componente **não renderiza nada até o JS confirmar a hora**. Um HTML\n * estático dizendo \"aberto\" é uma afirmação que ninguém verificou — e no\n * primeiro print de tela errado vira reclamação. Nada na tela é melhor que\n * horário errado na tela.\n *\n * Feriado não é calculável com o dado que a maioria dos clientes tem. Diga isso\n * em texto ao lado (`note`) em vez de fingir precisão.\n *\n * ── O que ele calculava e jogava fora ──────────────────────────────────────\n *\n * A peça já resolvia a hora no fuso certo e devolvia UMA linha: \"aberto\" ou\n * \"fechado\". Todo o trabalho caro — a conta que ninguém vê — morria num\n * adjetivo, e o adjetivo sozinho não decide nada: quem lê \"aberto\" às 17h50\n * pega o carro e chega com a porta fechada, e quem lê \"fechado\" não sabe se\n * volta em vinte minutos ou na segunda.\n *\n * Agora o mesmo cálculo entrega as três coisas que a pessoa está de fato\n * perguntando:\n *\n * 1. **Quanto falta pra fechar** — e um estado próprio (`closing`) na última\n *    hora, porque \"aberto por 8 minutos\" e \"aberto por 6 horas\" não são a mesma\n *    informação e não podem ter a mesma cara.\n * 2. **Quando reabre** — hoje, amanhã ou no próximo dia que `openDays`\n *    permitir. A busca anda dia a dia até achar um aberto, então loja que fecha\n *    domingo e segunda responde \"terça\" sem ninguém escrever a regra.\n * 3. **Que a resposta é de agora.** O ponto pulsa enquanto está aberto — é o\n *    único jeito de um dado calculado parecer vivo em vez de impresso. Ele\n *    para inteiro em `prefers-reduced-motion`.\n *\n * Tudo em `labels`, e as duas chaves antigas (`open`/`closed`) continuam com a\n * mesma assinatura: quem já traduziu a peça não recebe inglês de volta.\n */\n\nexport interface StoreStatusLabels {\n  /** Linha principal, aberto. Recebe a hora de fechar. */\n  open: (closeHour: number) => string;\n  /** Linha principal, fechado. Recebe a hora de abrir. */\n  closed: (openHour: number) => string;\n  /** Linha de baixo, aberto: quanto falta pra fechar. */\n  closesIn: (falta: { horas: number; minutos: number }) => string;\n  /** Linha de baixo, fechado: quando reabre. `dia` é `null` quando é hoje. */\n  opensAt: (quando: { hora: string; dia: string | null; emDias: number }) => string;\n  /** Como a hora cheia se escreve: 9 → \"9am\". */\n  hour: (h: number) => string;\n}\n\nexport interface StoreStatusProps extends React.HTMLAttributes<HTMLParagraphElement> {\n  /** Fuso IANA da loja, ex.: \"Europe/London\". */\n  timeZone: string;\n  openHour?: number;\n  closeHour?: number;\n  /** Dias abertos, 0 = domingo. Padrão: todos. */\n  openDays?: number[];\n  /** Locale do nome do dia da semana. */\n  locale?: string;\n  /** Minutos que fazem o estado virar `closing`. */\n  closingSoon?: number;\n  labels?: Partial<StoreStatusLabels>;\n}\n\nconst fmtHour = (h: number) => (h > 12 ? `${h - 12}pm` : h === 12 ? \"12pm\" : `${h}am`);\n\nconst PADRAO: StoreStatusLabels = {\n  open: () => \"Open now\",\n  closed: () => \"Closed\",\n  closesIn: ({ horas, minutos }) =>\n    `Closes in ${horas ? `${horas}h${String(minutos).padStart(2, \"0\")}` : `${minutos}min`}`,\n  opensAt: ({ hora, dia, emDias }) =>\n    emDias === 0\n      ? `Opens at ${hora}`\n      : emDias === 1\n        ? `Opens tomorrow at ${hora}`\n        : `Opens ${dia} at ${hora}`,\n  hour: fmtHour,\n};\n\nconst DIAS = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\n\ninterface Leitura {\n  estado: \"open\" | \"closing\" | \"closed\";\n  /** Minutos até fechar. Só existe com a loja aberta. */\n  falta: number;\n  /** Em quantos dias reabre, e o nome do dia. Só existe com a loja fechada. */\n  emDias: number;\n  dia: string | null;\n}\n\nexport function StoreStatus({\n  timeZone,\n  openHour = 9,\n  closeHour = 17,\n  openDays,\n  locale = \"en-GB\",\n  closingSoon = 60,\n  labels,\n  className,\n  ...props\n}: StoreStatusProps) {\n  const [leitura, setLeitura] = React.useState<Leitura | null>(null);\n  const texto = { ...PADRAO, ...labels };\n\n  // `openDays` costuma chegar como literal (`[1,2,3]`), que é um array novo a\n  // cada render do pai. Comparar pela chave em vez do objeto evita reagendar o\n  // relógio sessenta vezes por minuto sem nada ter mudado.\n  const chaveDias = openDays?.join(\",\") ?? \"\";\n\n  React.useEffect(() => {\n    const dias = chaveDias ? chaveDias.split(\",\").map(Number) : null;\n\n    const ler = () => {\n      const parts = new Intl.DateTimeFormat(\"en-GB\", {\n        timeZone,\n        hour: \"numeric\",\n        minute: \"numeric\",\n        hourCycle: \"h23\",\n        weekday: \"short\",\n      }).formatToParts(new Date());\n\n      const hora = Number(parts.find((p) => p.type === \"hour\")?.value);\n      const min = Number(parts.find((p) => p.type === \"minute\")?.value);\n      if (!Number.isFinite(hora) || !Number.isFinite(min)) return;\n\n      const wd = parts.find((p) => p.type === \"weekday\")?.value ?? \"\";\n      const diaIdx = DIAS.indexOf(wd);\n      const hojeAbre = !dias || dias.includes(diaIdx);\n\n      const agora = hora * 60 + min;\n      const abre = openHour * 60;\n      const fecha = closeHour * 60;\n\n      if (hojeAbre && agora >= abre && agora < fecha) {\n        const falta = fecha - agora;\n        setLeitura({\n          estado: falta <= closingSoon ? \"closing\" : \"open\",\n          falta,\n          emDias: 0,\n          dia: null,\n        });\n        return;\n      }\n\n      // Fechado: o próximo dia útil da loja. Anda dia a dia porque a regra é\n      // uma lista de dias, não um intervalo — loja que abre só sábado também\n      // tem resposta certa aqui.\n      let emDias = -1;\n      if (hojeAbre && agora < abre) emDias = 0;\n      else {\n        for (let d = 1; d <= 7; d++) {\n          if (!dias || dias.includes((diaIdx + d) % 7)) {\n            emDias = d;\n            break;\n          }\n        }\n      }\n\n      const dia =\n        emDias > 1\n          ? new Intl.DateTimeFormat(locale, { timeZone, weekday: \"long\" }).format(\n              new Date(Date.now() + emDias * 86_400_000),\n            )\n          : null;\n\n      setLeitura({ estado: \"closed\", falta: 0, emDias, dia });\n    };\n\n    ler();\n    // Um minuto: é a menor unidade que a peça mostra. Contar segundo a segundo\n    // acordaria a aba sessenta vezes pra redesenhar o mesmo texto.\n    const t = setInterval(ler, 60_000);\n    return () => clearInterval(t);\n  }, [timeZone, openHour, closeHour, chaveDias, locale, closingSoon]);\n\n  // Até o JS confirmar, a página não afirma nada.\n  if (leitura === null) return null;\n\n  const aberto = leitura.estado !== \"closed\";\n\n  /*\n   * A linha de baixo só aparece se ela puder falar o mesmo idioma da de cima.\n   *\n   * Quem já traduziu a peça passou `open`/`closed` e nada mais — o padrão das\n   * chaves novas é inglês, e cair nele imprimiria \"Closes in 2h14\" embaixo de\n   * \"Atende agora\". Meia tradução é pior que a linha ausente: a de cima\n   * continua sendo a resposta, e a peça volta a ser exatamente o que era antes\n   * pra quem não pediu nada.\n   */\n  const podeDetalhar =\n    !(labels?.open || labels?.closed) || Boolean(labels?.closesIn || labels?.opensAt);\n\n  const detalhe = aberto\n    ? texto.closesIn({ horas: Math.floor(leitura.falta / 60), minutos: leitura.falta % 60 })\n    : texto.opensAt({\n        hora: texto.hour(openHour),\n        dia: leitura.dia,\n        emDias: leitura.emDias,\n      });\n\n  return (\n    <p\n      data-state={leitura.estado}\n      className={cn(\"inline-flex items-center gap-2.5\", className)}\n      {...props}\n    >\n      <span aria-hidden=\"true\" className=\"relative flex size-2 shrink-0\">\n        {aberto && (\n          /* O halo é o que faz a leitura parecer de AGORA. Ele nasce do mesmo\n             ponto e some pra fora, então não muda uma linha de layout — e em\n             `closing` bate mais rápido, porque a última hora é a informação\n             que a pessoa precisa notar sem ler. */\n          <span\n            className={cn(\n              \"absolute inset-0 animate-ping rounded-full opacity-60 motion-reduce:hidden\",\n              \"bg-[var(--brand)]\",\n              leitura.estado === \"closing\" && \"[animation-duration:1s]\",\n            )}\n          />\n        )}\n        <span\n          className={cn(\n            \"relative size-2 rounded-full\",\n            aberto ? \"bg-[var(--brand)]\" : \"bg-[var(--muted-foreground)]\",\n          )}\n        />\n      </span>\n\n      <span className=\"flex min-w-0 flex-col gap-0.5\">\n        <span className=\"text-[12px] font-bold uppercase leading-none tracking-[0.1em]\">\n          {aberto ? texto.open(closeHour) : texto.closed(openHour)}\n        </span>\n        {podeDetalhar && (\n          <span className=\"text-[12px] leading-none text-[var(--muted-foreground)] tabular-nums\">\n            {detalhe}\n          </span>\n        )}\n      </span>\n    </p>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Mostrar aberto ou fechado de loja física calculado no fuso dela, em site de varejo ou restaurante",
      "evite": "Status de processo ou sistema: status-ticker; contagem até um horário, countdown"
    }
  },
  "categories": [
    "conteudo",
    "loja",
    "fuso"
  ],
  "type": "registry:ui"
}