{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "odometer",
  "type": "registry:component",
  "title": "Odometer",
  "description": "Numbers that roll digit by digit up to their value, like a mechanical odometer.",
  "author": "Marcelo Treviño Velázquez <https://furin.dev>",
  "dependencies": [],
  "categories": [
    "Text"
  ],
  "docs": "Installed in components/furin/odometer/. Docs, live controls and props table: https://furin.dev/components/odometer/",
  "files": [
    {
      "path": "registry/odometer/Odometer.tsx",
      "content": "\"use client\";\nimport { useLayoutEffect, useState } from \"react\";\nimport type { CSSProperties } from \"react\";\nimport \"./Odometer.css\";\n\nexport interface OdometerProps {\n  /** Número que se muestra; si cambia, las columnas ruedan hasta el nuevo valor. */\n  value?: number;\n  /** Texto antes del número, por ejemplo \"$\". */\n  prefix?: string;\n  /** Texto después del número, por ejemplo \" MXN\" o \"%\". */\n  suffix?: string;\n  /** Separador de miles (\"\" para no separar). */\n  separator?: string;\n  /** Separador decimal. */\n  decimalSeparator?: string;\n  /** Cuántos decimales se muestran. */\n  decimals?: number;\n  /** Duración del giro de cada columna, en milisegundos. */\n  duration?: number;\n  /** Retraso extra entre una columna y la siguiente (de izquierda a derecha), en milisegundos. */\n  stagger?: number;\n  /** Curva de la animación (cualquier valor válido de transition-timing-function). */\n  easing?: string;\n  /** Si es true, al montar rueda desde cero hasta el valor. */\n  animateOnMount?: boolean;\n  /** Color de los dígitos (por defecto hereda el del texto). */\n  color?: string;\n  /** Color del prefijo y el sufijo (por defecto, el del texto atenuado). */\n  affixColor?: string;\n  /** Clase CSS extra para el contenedor. */\n  className?: string;\n  /** Estilos en línea del contenedor (aquí va el tamaño y la fuente). */\n  style?: CSSProperties;\n}\n\nconst DIGITOS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];\n\n/** Da formato al número: miles agrupados, decimales fijos y signo. */\nfunction formatear(valor: number, decimales: number, separador: string, decimal: string): string {\n  const seguro = Number.isFinite(valor) ? valor : 0;\n  const [entero, fraccion] = Math.abs(seguro).toFixed(Math.max(0, Math.min(decimales, 20))).split(\".\");\n  const agrupado = entero.replace(/\\B(?=(\\d{3})+(?!\\d))/g, separador);\n  const signo = seguro < 0 && Number(Math.abs(seguro).toFixed(decimales)) !== 0 ? \"-\" : \"\";\n  return signo + agrupado + (fraccion ? decimal + fraccion : \"\");\n}\n\n/** Una columna con los dígitos del 0 al 9; solo se ve uno a la vez. */\nfunction Columna({ digito, retraso }: { digito: number; retraso: number }) {\n  return (\n    <span className=\"furin-odometer__columna\">\n      <span\n        className=\"furin-odometer__tira\"\n        // Cada dígito mide el 10 % de la tira, así que subir \"n\" dígitos es -10 % × n.\n        style={{ transform: `translateY(${digito * -10}%)`, transitionDelay: `${retraso}ms` }}\n      >\n        {DIGITOS.map((d) => (\n          <span key={d} className=\"furin-odometer__digito\">\n            {d}\n          </span>\n        ))}\n      </span>\n    </span>\n  );\n}\n\nexport function Odometer({\n  value = 0,\n  prefix = \"\",\n  suffix = \"\",\n  separator = \",\",\n  decimalSeparator = \".\",\n  decimals = 0,\n  duration = 1800,\n  stagger = 90,\n  easing = \"cubic-bezier(0.16, 1, 0.3, 1)\",\n  animateOnMount = true,\n  color,\n  affixColor,\n  className = \"\",\n  style,\n}: OdometerProps) {\n  // enCeros: las columnas se muestran en 0 antes de rodar hacia el valor.\n  // animar: las transiciones solo se activan después de montar, para que el\n  // salto inicial a ceros no se vea (ni con el HTML que llega del servidor).\n  const [enCeros, setEnCeros] = useState(false);\n  const [animar, setAnimar] = useState(false);\n\n  useLayoutEffect(() => {\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n    if (animateOnMount) setEnCeros(true);\n\n    // Se espera a que el navegador pinte los ceros antes de pedir el giro.\n    let segundo = 0;\n    const primero = requestAnimationFrame(() => {\n      segundo = requestAnimationFrame(() => {\n        setAnimar(true);\n        setEnCeros(false);\n      });\n    });\n    return () => {\n      cancelAnimationFrame(primero);\n      cancelAnimationFrame(segundo);\n    };\n  }, [animateOnMount]);\n\n  const texto = formatear(value, decimals, separator, decimalSeparator);\n  const simbolos = Array.from(texto);\n\n  const variables = {\n    \"--furin-odometer-color\": color,\n    \"--furin-odometer-afijo\": affixColor,\n    \"--furin-odometer-duracion\": `${duration}ms`,\n    \"--furin-odometer-curva\": easing,\n    ...style,\n  } as CSSProperties;\n\n  return (\n    <span className={`furin-odometer${animar ? \" furin-odometer--animado\" : \"\"} ${className}`.trim()} style={variables}>\n      {/* Lo que leen los lectores de pantalla: el valor final, y avisa solo cuando cambia. */}\n      <span className=\"furin-odometer__lectura\" aria-live=\"polite\" aria-atomic=\"true\">\n        {prefix}\n        {texto}\n        {suffix}\n      </span>\n      <span className=\"furin-odometer__visual\" aria-hidden=\"true\">\n        {prefix && <span className=\"furin-odometer__afijo\">{prefix}</span>}\n        {simbolos.map((simbolo, i) => {\n          // La clave cuenta desde la derecha: si el número gana una cifra, las\n          // columnas existentes conservan su identidad y ruedan en lugar de reiniciarse.\n          const desdeLaDerecha = simbolos.length - 1 - i;\n          if (!/\\d/.test(simbolo)) {\n            return (\n              <span key={`s${desdeLaDerecha}`} className=\"furin-odometer__simbolo\">\n                {simbolo}\n              </span>\n            );\n          }\n          return (\n            <Columna\n              key={`d${desdeLaDerecha}`}\n              digito={enCeros ? 0 : Number(simbolo)}\n              retraso={i * stagger}\n            />\n          );\n        })}\n        {suffix && <span className=\"furin-odometer__afijo\">{suffix}</span>}\n      </span>\n    </span>\n  );\n}\n\nexport default Odometer;\n",
      "type": "registry:component",
      "target": "components/furin/odometer/Odometer.tsx"
    },
    {
      "path": "registry/odometer/Odometer.css",
      "content": ".furin-odometer {\n  position: relative;\n  display: inline-block;\n  color: var(--furin-odometer-color, inherit);\n  /* Cada celda mide 1.2em de alto; el dígito queda centrado y se asoman los vecinos. */\n  line-height: 1.2;\n  font-variant-numeric: tabular-nums;\n  font-feature-settings: \"tnum\" 1;\n  white-space: nowrap;\n}\n\n/* Valor final para lectores de pantalla (se anuncia cuando `value` cambia). */\n.furin-odometer__lectura {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  overflow: hidden;\n  clip-path: inset(50%);\n  white-space: nowrap;\n}\n\n.furin-odometer__visual {\n  display: inline-flex;\n  align-items: center;\n}\n\n.furin-odometer__afijo {\n  white-space: pre; /* conserva el espacio de \" MXN\" */\n  color: var(--furin-odometer-afijo, color-mix(in srgb, currentColor 55%, transparent));\n}\n\n.furin-odometer__simbolo {\n  text-align: center;\n}\n\n/* Ventana de una celda: solo deja ver un dígito, con los bordes difuminados. */\n.furin-odometer__columna {\n  display: block;\n  width: 1ch;\n  height: 1.2em;\n  overflow: hidden;\n  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 0.2em, #000 calc(100% - 0.2em), transparent 100%);\n  mask-image: linear-gradient(to bottom, transparent 0, #000 0.2em, #000 calc(100% - 0.2em), transparent 100%);\n}\n\n.furin-odometer__tira {\n  display: flex;\n  flex-direction: column;\n  will-change: transform;\n}\n\n/* Las transiciones solo existen una vez que el componente ya montó. */\n.furin-odometer--animado .furin-odometer__tira {\n  transition: transform var(--furin-odometer-duracion, 1800ms) var(--furin-odometer-curva, cubic-bezier(0.16, 1, 0.3, 1));\n}\n\n.furin-odometer__digito {\n  display: block;\n  height: 1.2em;\n  text-align: center;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .furin-odometer--animado .furin-odometer__tira {\n    transition: none;\n  }\n}\n",
      "type": "registry:file",
      "target": "components/furin/odometer/Odometer.css"
    }
  ]
}
