{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "timezone",
  "title": "Timezone",
  "description": "A component for displaying and converting timezones.",
  "dependencies": [
    "ms",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://optics.agusmayol.com.ar/r/tooltip.json",
    "https://optics.agusmayol.com.ar/r/button.json",
    "https://optics.agusmayol.com.ar/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/optics/timezone.jsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport ms from \"ms\";\nimport {\n\tTooltip,\n\tTooltipTrigger,\n\tTooltipContent,\n} from \"@/registry/optics/tooltip\";\nimport { Button } from \"@/registry/optics/button\";\nimport { cn } from \"@/registry/optics/lib/utils\";\n\n// Constantes de tiempo calculadas una vez para evitar recálculos\nconst ONE_SECOND = ms(\"1s\");\nconst ONE_MINUTE = ms(\"1m\");\nconst ONE_HOUR = ms(\"1h\");\nconst ONE_DAY = ms(\"1d\");\n\nfunction Timezone({\n\ttimestamp = Date.now(),\n\trender = null,\n\tclassName = \"\",\n\tside = \"top\",\n\tsideOffset = 4,\n\t...props\n}) {\n\tconst [userTimezone, setUserTimezone] = React.useState(null);\n\tconst [formattedUserTime, setFormattedUserTime] = React.useState(\"\");\n\tconst [formattedUtcTime, setFormattedUtcTime] = React.useState(\"\");\n\tconst [relativeTime, setRelativeTime] = React.useState(\"\");\n\tconst [isOpen, setIsOpen] = React.useState(false);\n\tconst [copiedButton, setCopiedButton] = React.useState(null);\n\tconst [isExiting, setIsExiting] = React.useState(false);\n\tconst exitTimeoutRef = React.useRef(null);\n\tconst resetTimeoutRef = React.useRef(null);\n\n\t// Memoizar los formatters para evitar recrearlos en cada render\n\tconst utcFormatter = React.useMemo(\n\t\t() =>\n\t\t\tnew Intl.DateTimeFormat(\"en-US\", {\n\t\t\t\ttimeZone: \"UTC\",\n\t\t\t\tyear: \"numeric\",\n\t\t\t\tmonth: \"short\",\n\t\t\t\tday: \"numeric\",\n\t\t\t\thour: \"numeric\",\n\t\t\t\tminute: \"2-digit\",\n\t\t\t\tsecond: \"2-digit\",\n\t\t\t\thour12: true,\n\t\t\t}),\n\t\t[],\n\t);\n\n\tconst userFormatter = React.useMemo(\n\t\t() =>\n\t\t\tuserTimezone\n\t\t\t\t? new Intl.DateTimeFormat(\"en-US\", {\n\t\t\t\t\t\ttimeZone: userTimezone,\n\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\tmonth: \"short\",\n\t\t\t\t\t\tday: \"numeric\",\n\t\t\t\t\t\thour: \"numeric\",\n\t\t\t\t\t\tminute: \"2-digit\",\n\t\t\t\t\t\tsecond: \"2-digit\",\n\t\t\t\t\t\thour12: true,\n\t\t\t\t\t})\n\t\t\t\t: null,\n\t\t[userTimezone],\n\t);\n\n\tReact.useEffect(() => {\n\t\t// Obtener el huso horario del usuario de manera no invasiva\n\t\tif (typeof window !== \"undefined\") {\n\t\t\tconst timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n\t\t\tsetUserTimezone(timeZone);\n\t\t}\n\t}, []);\n\n\t// Función para actualizar los valores de tiempo\n\tconst updateTimeValues = React.useCallback(() => {\n\t\tif (!timestamp || !userTimezone || !userFormatter) return;\n\n\t\t// Convertir timestamp a Date\n\t\t// Acepta: string ISO (timestampz), número en milisegundos, o número en segundos\n\t\tlet date;\n\t\tif (typeof timestamp === \"string\") {\n\t\t\t// String ISO (timestampz) o timestamp en string\n\t\t\tdate = new Date(timestamp);\n\t\t} else if (typeof timestamp === \"number\") {\n\t\t\t// Si es menor a 1e12, asumimos que está en segundos y lo convertimos a milisegundos\n\t\t\t// Si es mayor o igual, asumimos que ya está en milisegundos\n\t\t\tdate = new Date(timestamp < 1e12 ? timestamp * 1000 : timestamp);\n\t\t} else {\n\t\t\tdate = new Date(timestamp);\n\t\t}\n\n\t\t// Validar que la fecha sea válida\n\t\tif (isNaN(date.getTime())) {\n\t\t\tsetFormattedUserTime(\"Invalid date\");\n\t\t\tsetFormattedUtcTime(\"Invalid date\");\n\t\t\tsetRelativeTime(\"Invalid date\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Formatear fecha en UTC\n\t\tsetFormattedUtcTime(utcFormatter.format(date));\n\n\t\t// Formatear fecha en la zona horaria del usuario\n\t\tsetFormattedUserTime(userFormatter.format(date));\n\n\t\t// Calcular tiempo relativo usando constantes precalculadas\n\t\tconst now = new Date();\n\t\tconst diffInMs = date.getTime() - now.getTime();\n\t\tconst absDiffInMs = Math.abs(diffInMs);\n\t\tconst isPast = diffInMs < 0;\n\t\tconst direction = isPast ? \"ago\" : \"in\";\n\n\t\tlet relative;\n\n\t\t// Si está dentro de los minutos (< 1 hora), mostrar minutos y segundos\n\t\tif (absDiffInMs < ONE_HOUR) {\n\t\t\tconst minutes = Math.floor(absDiffInMs / ONE_MINUTE);\n\t\t\tconst remainingMs = absDiffInMs % ONE_MINUTE;\n\t\t\tconst seconds = Math.floor(remainingMs / ONE_SECOND);\n\n\t\t\tif (seconds > 0 && minutes > 0) {\n\t\t\t\t// Mostrar minutos y segundos\n\t\t\t\tconst minutesStr = ms(minutes * ONE_MINUTE, { long: true });\n\t\t\t\tconst secondsStr = ms(seconds * ONE_SECOND, { long: true });\n\t\t\t\trelative = `${minutesStr} ${secondsStr} ${direction}`;\n\t\t\t} else if (minutes > 0) {\n\t\t\t\t// Solo minutos\n\t\t\t\trelative = `${ms(minutes * ONE_MINUTE, { long: true })} ${direction}`;\n\t\t\t} else {\n\t\t\t\t// Solo segundos\n\t\t\t\trelative = `${ms(absDiffInMs, { long: true })} ${direction}`;\n\t\t\t}\n\t\t}\n\t\t// Si está dentro de las horas (< 24 horas), mostrar horas y minutos\n\t\telse if (absDiffInMs < ONE_DAY) {\n\t\t\tconst hours = Math.floor(absDiffInMs / ONE_HOUR);\n\t\t\tconst remainingMs = absDiffInMs % ONE_HOUR;\n\t\t\tconst minutes = Math.floor(remainingMs / ONE_MINUTE);\n\n\t\t\tif (minutes > 0) {\n\t\t\t\t// Mostrar horas y minutos\n\t\t\t\tconst hoursStr = ms(hours * ONE_HOUR, { long: true });\n\t\t\t\tconst minutesStr = ms(minutes * ONE_MINUTE, { long: true });\n\t\t\t\trelative = `${hoursStr} ${minutesStr} ${direction}`;\n\t\t\t} else {\n\t\t\t\t// Solo horas\n\t\t\t\trelative = `${ms(hours * ONE_HOUR, { long: true })} ${direction}`;\n\t\t\t}\n\t\t}\n\t\t// Para otros casos, usar el string completo de ms\n\t\telse {\n\t\t\trelative = `${ms(absDiffInMs, { long: true })} ${direction}`;\n\t\t}\n\n\t\tsetRelativeTime(relative);\n\t}, [timestamp, userTimezone, userFormatter, utcFormatter]);\n\n\t// Actualizar cuando cambian timestamp o userTimezone\n\tReact.useEffect(() => {\n\t\tupdateTimeValues();\n\t}, [updateTimeValues]);\n\n\t// Actualizar en tiempo real mientras el tooltip esté abierto\n\tReact.useEffect(() => {\n\t\tif (!isOpen) return;\n\n\t\t// Actualizar inmediatamente\n\t\tupdateTimeValues();\n\n\t\t// Configurar intervalo para actualizar cada segundo\n\t\tconst interval = setInterval(() => {\n\t\t\tupdateTimeValues();\n\t\t}, 1000);\n\n\t\t// Limpiar intervalo cuando el tooltip se cierre\n\t\treturn () => {\n\t\t\tclearInterval(interval);\n\t\t};\n\t}, [isOpen, updateTimeValues]);\n\n\tconst clearCopyTimers = React.useCallback(() => {\n\t\tif (exitTimeoutRef.current) {\n\t\t\tclearTimeout(exitTimeoutRef.current);\n\t\t\texitTimeoutRef.current = null;\n\t\t}\n\t\tif (resetTimeoutRef.current) {\n\t\t\tclearTimeout(resetTimeoutRef.current);\n\t\t\tresetTimeoutRef.current = null;\n\t\t}\n\t}, []);\n\n\t// Función para copiar al portapapeles\n\tconst copyToClipboard = React.useCallback(\n\t\tasync (text, buttonId) => {\n\t\t\tif (\n\t\t\t\t!text ||\n\t\t\t\ttext === \"—\" ||\n\t\t\t\ttext === \"Invalid date\" ||\n\t\t\t\ttext === \"Loading...\"\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tawait navigator.clipboard.writeText(text);\n\t\t\t\tclearCopyTimers();\n\t\t\t\tsetIsExiting(false);\n\t\t\t\tsetCopiedButton(buttonId);\n\t\t\t\t// Iniciar animación de salida después de 1.5 segundos\n\t\t\t\texitTimeoutRef.current = setTimeout(() => {\n\t\t\t\t\tsetIsExiting(true);\n\t\t\t\t\t// Resetear el estado después de la animación de salida (300ms)\n\t\t\t\t\tresetTimeoutRef.current = setTimeout(() => {\n\t\t\t\t\t\tsetCopiedButton(null);\n\t\t\t\t\t\tsetIsExiting(false);\n\t\t\t\t\t}, 300);\n\t\t\t\t}, 1500);\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(\"Error al copiar al portapapeles:\", err);\n\t\t\t}\n\t\t},\n\t\t[clearCopyTimers],\n\t);\n\n\tReact.useEffect(() => {\n\t\treturn () => {\n\t\t\tclearCopyTimers();\n\t\t};\n\t}, [clearCopyTimers]);\n\n\treturn (\n\t\t<Tooltip open={isOpen} onOpenChange={setIsOpen}>\n\t\t\t<TooltipTrigger render={render} />\n\t\t\t<TooltipContent\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"w-full max-w-none min-w-sm h-full hover:bg-background\",\n\t\t\t\t\tclassName,\n\t\t\t\t\t\"p-1\",\n\t\t\t\t)}\n\t\t\t\tside={side}\n\t\t\t\tsideOffset={sideOffset}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t<div className=\"w-full flex flex-col gap-0.5 py-0 \">\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tclassName=\"font-normal\"\n\t\t\t\t\t\tanimation=\"colors\"\n\t\t\t\t\t\tonClick={() => copyToClipboard(formattedUserTime, \"user\")}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div className=\"w-full flex text-center justify-start gap-2 text-xs\">\n\t\t\t\t\t\t\t<div className=\"text-start text-muted-foreground w-40\">\n\t\t\t\t\t\t\t\t{userTimezone || \"Loading...\"}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<p\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"tabular-nums font-mono transition-all duration-300 text-foreground\",\n\t\t\t\t\t\t\t\t\tcopiedButton === \"user\"\n\t\t\t\t\t\t\t\t\t\t? isExiting\n\t\t\t\t\t\t\t\t\t\t\t? \"opacity-0 scale-95\"\n\t\t\t\t\t\t\t\t\t\t\t: \"opacity-100 scale-100 animate-in fade-in-0 zoom-in-95\"\n\t\t\t\t\t\t\t\t\t\t: \"opacity-100 scale-100\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{copiedButton === \"user\" ? \"Copied!\" : formattedUserTime || \"—\"}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Button>\n\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tclassName=\"font-normal\"\n\t\t\t\t\t\tanimation=\"colors\"\n\t\t\t\t\t\tonClick={() => copyToClipboard(formattedUtcTime, \"utc\")}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div className=\"w-full flex text-center justify-start gap-2 text-xs\">\n\t\t\t\t\t\t\t<div className=\"text-start text-muted-foreground w-40\">UTC</div>\n\t\t\t\t\t\t\t<p\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"tabular-nums font-mono transition-all duration-300 text-foreground\",\n\t\t\t\t\t\t\t\t\tcopiedButton === \"utc\"\n\t\t\t\t\t\t\t\t\t\t? isExiting\n\t\t\t\t\t\t\t\t\t\t\t? \"opacity-0 scale-95\"\n\t\t\t\t\t\t\t\t\t\t\t: \"opacity-100 scale-100 animate-in fade-in-0 zoom-in-95\"\n\t\t\t\t\t\t\t\t\t\t: \"opacity-100 scale-100\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{copiedButton === \"utc\" ? \"Copied!\" : formattedUtcTime || \"—\"}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Button>\n\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tclassName=\"font-normal z-10\"\n\t\t\t\t\t\tanimation=\"colors\"\n\t\t\t\t\t\tonClick={() => copyToClipboard(relativeTime, \"relative\")}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div className=\"w-full flex text-center justify-start gap-2 text-xs\">\n\t\t\t\t\t\t\t<div className=\"text-start text-muted-foreground w-40\">\n\t\t\t\t\t\t\t\tRelative\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<p\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"tabular-nums font-mono transition-all duration-300 text-foreground\",\n\t\t\t\t\t\t\t\t\tcopiedButton === \"relative\"\n\t\t\t\t\t\t\t\t\t\t? isExiting\n\t\t\t\t\t\t\t\t\t\t\t? \"opacity-0 scale-95\"\n\t\t\t\t\t\t\t\t\t\t\t: \"opacity-100 scale-100 animate-in fade-in-0 zoom-in-95\"\n\t\t\t\t\t\t\t\t\t\t: \"opacity-100 scale-100\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{copiedButton === \"relative\" ? \"Copied!\" : relativeTime || \"—\"}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</TooltipContent>\n\t\t</Tooltip>\n\t);\n}\n\nTimezone.displayName = \"Timezone\";\n\nexport { Timezone };\n",
      "type": "registry:component",
      "target": "components/optics/timezone.jsx"
    }
  ],
  "type": "registry:component"
}