{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"binary-year","type":"registry:component","title":"Binary year","description":"A year written in binary; reach for it and a wave runs in from the right, the leading digits fade to zeros and the year you know types itself in.","author":"Yash Bavadiya <https://xevrion.dev>","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"components/binary-year.tsx","type":"registry:component","content":"\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { usePreviewPlay } from \"@/lib/preview-play\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { cn } from \"@/lib/utils\";\n\n// After George's personal-site easter egg (https://x.com/vanjek): a year\n// written in binary. Reach for it and a wave runs in from the right; each\n// digit flickers once and lands, the leading ones fade to quiet zeros, and\n// the year you'd recognise types itself in at the end. Leave, and it rolls\n// back to binary the other way.\n\n// Between one digit landing and the next starting, in ms: quick enough to\n// read as one sweep, slow enough to see it travel.\nconst STAGGER = 42;\n// How long a single digit flickers before it lands, and how often it\n// changes while it does.\nconst FLICKER = 120;\nconst TICK = 40;\n\ntype Look = \"rest\" | \"faint\" | \"strong\" | \"live\";\n\nexport function BinaryYear({\n  year = 2026,\n  label = \"ui lab, est.\",\n  className,\n}: {\n  year?: number;\n  label?: string;\n  className?: string;\n}) {\n  const reduceMotion = useReducedMotion();\n  const play = usePreviewPlay();\n  const [own, setOwn] = useState(false);\n  const [show, setShow] = useState(false);\n  const decoded = play ? show : own;\n\n  const binary = year.toString(2);\n  const width = binary.length;\n  const decimal = String(year).padStart(width, \"0\");\n  // The real digits start where the padding stops.\n  const firstReal = width - String(year).length;\n\n  const slots = useRef<(HTMLSpanElement | null)[]>([]);\n  const timers = useRef<ReturnType<typeof setTimeout>[]>([]);\n  const mounted = useRef(false);\n  const touched = useRef(false);\n\n  const paint = (i: number, char: string, look: Look) => {\n    const el = slots.current[i];\n    if (!el) return;\n    el.textContent = char;\n    el.dataset.look = look;\n  };\n\n  useEffect(() => {\n    // The first render already shows binary; nothing to animate.\n    if (!mounted.current) {\n      mounted.current = true;\n      return;\n    }\n    timers.current.forEach(clearTimeout);\n    timers.current = [];\n    const target = (i: number): [string, Look] =>\n      decoded\n        ? [decimal[i], i < firstReal ? \"faint\" : \"strong\"]\n        : [binary[i], \"rest\"];\n\n    if (reduceMotion) {\n      for (let i = 0; i < width; i++) paint(i, ...target(i));\n      return;\n    }\n\n    // Decoding sweeps in from the right, where the real digits are;\n    // encoding goes back from the left.\n    for (let n = 0; n < width; n++) {\n      const i = decoded ? width - 1 - n : n;\n      const start = n * STAGGER;\n      for (let t = 0; t < FLICKER; t += TICK) {\n        timers.current.push(\n          setTimeout(() => {\n            // While it flickers, it shows what kind of digit it's\n            // becoming: only 0s and 1s on the way back to binary.\n            const pool = decoded ? \"0123456789\" : \"01\";\n            paint(i, pool[Math.floor(Math.random() * pool.length)], \"live\");\n          }, start + t),\n        );\n      }\n      timers.current.push(setTimeout(() => paint(i, ...target(i)), start + FLICKER));\n    }\n    // binary, decimal and the rest are derived from props that stay put\n    // while it runs.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [decoded, reduceMotion]);\n\n  useEffect(() => () => timers.current.forEach(clearTimeout), []);\n\n  // The card's show: decode, hold, encode.\n  useEffect(() => {\n    if (!play) return;\n    let on = false;\n    const id = setInterval(() => {\n      on = !on;\n      setShow(on);\n    }, 1900);\n    return () => {\n      clearInterval(id);\n      setShow(false);\n    };\n  }, [play]);\n\n  return (\n    <button\n      type=\"button\"\n      aria-pressed={own}\n      onPointerEnter={(e) => e.pointerType !== \"touch\" && setOwn(true)}\n      onPointerLeave={(e) => e.pointerType !== \"touch\" && setOwn(false)}\n      onPointerDown={(e) => {\n        touched.current = e.pointerType === \"touch\";\n      }}\n      // A mouse decodes it by hovering; a tap or Enter toggles it. Safari's\n      // click doesn't say what made it, so the press remembers.\n      onClick={(e) => {\n        if (e.detail === 0 || touched.current) setOwn((o) => !o);\n      }}\n      // Keyboard focus decodes; the focus a tap brings with it doesn't, or\n      // the tap's own toggle would undo it straight away.\n      onFocus={(e) => e.currentTarget.matches(\":focus-visible\") && setOwn(true)}\n      onBlur={() => setOwn(false)}\n      className={cn(\n        \"group/year flex touch-manipulation items-baseline gap-[0.45em] rounded-lg px-2 py-1 text-[22px] outline-hidden select-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-4 focus-visible:outline-foreground\",\n        className,\n      )}\n    >\n      <span className=\"text-muted-foreground\">{label}</span>\n      <span className=\"sr-only\">{year}</span>\n      <span aria-hidden className=\"flex font-mono tracking-[0.02em] tabular-nums\">\n        {[...binary].map((char, i) => (\n          <span\n            key={i}\n            ref={(el) => {\n              slots.current[i] = el;\n            }}\n            data-look=\"rest\"\n            // Rest is the binary in a quiet grey; faint is the padding zeros\n            // once decoded; strong is the year itself; live is a digit\n            // mid-flicker, briefly the darkest thing on the line.\n            className=\"w-[1ch] text-center transition-[color,opacity] duration-150 ease-out data-[look=faint]:text-foreground data-[look=faint]:opacity-[0.22] data-[look=live]:text-foreground data-[look=rest]:text-muted-foreground data-[look=strong]:text-foreground\"\n          >\n            {char}\n          </span>\n        ))}\n      </span>\n    </button>\n  );\n}\n\nexport default function BinaryYearDemo() {\n  return <BinaryYear />;\n}\n"},{"path":"lib/preview-play.ts","type":"registry:lib","target":"lib/preview-play.ts","content":"\"use client\";\n\nimport { createContext, useContext } from \"react\";\n\n// Lets a demo know whether it's showing in an index card, and whether that\n// card is hovered or focused. Demos use it to act themselves out on hover:\n// a star button stars itself, a checkbox scribbles, and so on.\n//\n// null: not in a preview (the component's own page, or real use).\n// false: in a preview, at rest.\n// true: in a preview, being hovered or focused: play the show.\nexport const PreviewPlayContext = createContext<boolean | null>(null);\n\nexport function usePreviewPlay() {\n  return useContext(PreviewPlayContext);\n}\n"},{"path":"hooks/use-reduced-motion.ts","type":"registry:hook","target":"hooks/use-reduced-motion.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// Motion's useReducedMotion reads the media query on the very first client\n// render, but the server can't know it, so every component that renders\n// differently under reduced motion broke hydration for those users. This\n// reports false during hydration, matching the server HTML, then the real\n// preference straight after, and follows it if it changes.\nconst query = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribe(onChange: () => void) {\n  const media = window.matchMedia(query);\n  media.addEventListener(\"change\", onChange);\n  return () => media.removeEventListener(\"change\", onChange);\n}\n\nexport function useReducedMotion() {\n  return useSyncExternalStore(\n    subscribe,\n    () => window.matchMedia(query).matches,\n    () => false,\n  );\n}\n"}],"categories":["text"],"docs":"From ui lab: https://lab.xevrion.dev/lab/binary-year"}