{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "matrix-clock",
  "type": "registry:ui",
  "title": "Matrix Clock",
  "description": "A synchronized dot-matrix world clock with a smooth scramble reveal and live India time.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/matrix-clock.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { cn } from \"@/lib/utils\";\nimport { useTimeSource } from \"./use-time-source\";\n\ngsap.registerPlugin(useGSAP);\n\nexport interface MatrixClockCity {\n  label: string;\n  timeZone: string;\n}\n\nexport interface MatrixClockProps {\n  cities?: readonly MatrixClockCity[];\n  date?: Date | string | number;\n  running?: boolean;\n  hourFormat?: \"12\" | \"24\";\n  showSeconds?: boolean;\n  animated?: boolean;\n  scrambleDuration?: number;\n  scrambleInterval?: number;\n  replayKey?: string | number;\n  dotRadius?: number;\n  rowGap?: number;\n  inactiveOpacity?: number;\n  glow?: number;\n  color?: string;\n  backgroundColor?: string;\n  maxWidth?: number;\n  className?: string;\n  ariaLabel?: string;\n}\n\nexport const MATRIX_CLOCK_CITIES: readonly MatrixClockCity[] = [\n  { label: \"NEW YORK CITY\", timeZone: \"America/New_York\" },\n  { label: \"TOKYO\", timeZone: \"Asia/Tokyo\" },\n  { label: \"HAWAII\", timeZone: \"Pacific/Honolulu\" },\n  { label: \"LOS ANGELES\", timeZone: \"America/Los_Angeles\" },\n  { label: \"INDIA\", timeZone: \"Asia/Kolkata\" },\n];\n\nconst GLYPHS: Record<string, readonly number[]> = {\n  \" \": [0, 0, 0, 0, 0, 0, 0],\n  \"0\": [14, 17, 19, 21, 25, 17, 14],\n  \"1\": [4, 12, 4, 4, 4, 4, 14],\n  \"2\": [14, 17, 1, 2, 4, 8, 31],\n  \"3\": [30, 1, 1, 14, 1, 1, 30],\n  \"4\": [2, 6, 10, 18, 31, 2, 2],\n  \"5\": [31, 16, 16, 30, 1, 1, 30],\n  \"6\": [14, 16, 16, 30, 17, 17, 14],\n  \"7\": [31, 1, 2, 4, 8, 8, 8],\n  \"8\": [14, 17, 17, 14, 17, 17, 14],\n  \"9\": [14, 17, 17, 15, 1, 1, 14],\n  A: [14, 17, 17, 31, 17, 17, 17],\n  B: [30, 17, 17, 30, 17, 17, 30],\n  C: [14, 17, 16, 16, 16, 17, 14],\n  D: [30, 17, 17, 17, 17, 17, 30],\n  E: [31, 16, 16, 30, 16, 16, 31],\n  F: [31, 16, 16, 30, 16, 16, 16],\n  G: [14, 17, 16, 23, 17, 17, 15],\n  H: [17, 17, 17, 31, 17, 17, 17],\n  I: [14, 4, 4, 4, 4, 4, 14],\n  J: [7, 2, 2, 2, 18, 18, 12],\n  K: [17, 18, 20, 24, 20, 18, 17],\n  L: [16, 16, 16, 16, 16, 16, 31],\n  M: [17, 27, 21, 21, 17, 17, 17],\n  N: [17, 25, 21, 19, 17, 17, 17],\n  O: [14, 17, 17, 17, 17, 17, 14],\n  P: [30, 17, 17, 30, 16, 16, 16],\n  Q: [14, 17, 17, 17, 21, 18, 13],\n  R: [30, 17, 17, 30, 20, 18, 17],\n  S: [15, 16, 16, 14, 1, 1, 30],\n  T: [31, 4, 4, 4, 4, 4, 4],\n  U: [17, 17, 17, 17, 17, 17, 14],\n  V: [17, 17, 17, 17, 17, 10, 4],\n  W: [17, 17, 17, 21, 21, 21, 10],\n  X: [17, 17, 10, 4, 10, 17, 17],\n  Y: [17, 17, 10, 4, 4, 4, 4],\n  Z: [31, 1, 2, 4, 8, 16, 31],\n  \":\": [0, 4, 4, 0, 4, 4, 0],\n  \"-\": [0, 0, 0, 31, 0, 0, 0],\n  \".\": [0, 0, 0, 0, 0, 12, 12],\n  \"/\": [1, 1, 2, 4, 8, 16, 16],\n  \"?\": [14, 17, 1, 2, 4, 0, 4],\n};\n\nconst SCRAMBLE_GLYPHS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nfunction glyphPath(rows: readonly number[], radius: number) {\n  let path = \"\";\n  for (let y = 0; y < 7; y++) {\n    for (let x = 0; x < 5; x++) {\n      if (rows[y] & (1 << (4 - x)))\n        path += `M${x + 0.5 - radius},${y + 0.5}a${radius},${radius} 0 1,0 ${radius * 2},0a${radius},${radius} 0 1,0 ${-radius * 2},0`;\n    }\n  }\n  return path;\n}\n\nexport function MatrixClock({\n  cities = MATRIX_CLOCK_CITIES,\n  date,\n  running = true,\n  hourFormat = \"24\",\n  showSeconds = false,\n  animated = true,\n  scrambleDuration = 1.6,\n  scrambleInterval = 5,\n  replayKey = 0,\n  dotRadius = 0.36,\n  rowGap = 3,\n  inactiveOpacity = 0.13,\n  glow = 0.3,\n  color = \"#e8e8e8\",\n  backgroundColor = \"#000000\",\n  maxWidth = 640,\n  className,\n  ariaLabel = \"World clock\",\n}: MatrixClockProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const paintRef = React.useRef<((time: number | null) => void) | null>(null);\n  const playbackRef = React.useRef<(() => void) | null>(null);\n  const { time } = useTimeSource({ mode: \"clock\", duration: 0, date, running });\n  const selectedCities = React.useMemo(\n    () =>\n      cities.slice(0, 12).map((city) => {\n        let formatter: Intl.DateTimeFormat | null = null;\n        try {\n          formatter = new Intl.DateTimeFormat(\"en-GB\", {\n            hour: \"2-digit\",\n            minute: \"2-digit\",\n            ...(showSeconds ? { second: \"2-digit\" } : {}),\n            hourCycle: hourFormat === \"24\" ? \"h23\" : \"h12\",\n            timeZone: city.timeZone,\n          });\n        } catch {\n          formatter = null;\n        }\n        const label = city.label.trim() || city.timeZone;\n        const displayLabel = label\n          .normalize(\"NFKD\")\n          .replace(/[\\u0300-\\u036f]/g, \"\")\n          .toUpperCase()\n          .replace(/[^A-Z0-9 .:/-]/g, \"?\")\n          .slice(0, 28);\n        return { ...city, label, displayLabel, formatter };\n      }),\n    [cities, hourFormat, showSeconds],\n  );\n  const timeColumns = (showSeconds ? 8 : 5) + (hourFormat === \"12\" ? 3 : 0);\n  const columns =\n    timeColumns +\n    2 +\n    Math.max(12, ...selectedCities.map((city) => city.displayLabel.length)) +\n    3;\n  const pitch = 7 + bounded(rowGap, 3, 1, 8);\n  const height = Math.max(7, (selectedCities.length - 1) * pitch + 7);\n  const radius = bounded(dotRadius, 0.36, 0.15, 0.48);\n  const paths = React.useMemo(\n    () =>\n      Object.fromEntries(\n        Object.entries(GLYPHS).map(([glyph, rows]) => [\n          glyph,\n          glyphPath(rows, radius),\n        ]),\n      ),\n    [radius],\n  );\n  const inactivePath = React.useMemo(\n    () => glyphPath([31, 31, 31, 31, 31, 31, 31], radius),\n    [radius],\n  );\n  const displayTimes = selectedCities.map((city) =>\n    time === null || !city.formatter\n      ? `${showSeconds ? \"--:--:--\" : \"--:--\"}${hourFormat === \"12\" ? \" --\" : \"\"}`\n      : city.formatter.format(time).toUpperCase(),\n  );\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      if (!root) return;\n      const cells = Array.from(\n        root.querySelectorAll<SVGPathElement>(\"[data-matrix-cell]\"),\n      );\n      const rendered = cells.map(() => \"\");\n      let text = selectedCities.map(\n        (city) => `${\"-\".repeat(timeColumns)}  ${city.displayLabel}`.padEnd(columns),\n      );\n      const state = { progress: 1 };\n      let lastFrame = -1;\n      const paint = (force = false) => {\n        const frame = Math.floor(performance.now() / 45);\n        if (!force && frame === lastFrame) return;\n        lastFrame = frame;\n        cells.forEach((cell, index) => {\n          const row = Math.floor(index / columns);\n          const column = index % columns;\n          const threshold =\n            0.18 +\n            (column / columns) * 0.68 +\n            (row / Math.max(1, selectedCities.length)) * 0.1;\n          const settled = state.progress >= threshold;\n          const glyph = settled\n            ? text[row][column]\n            : SCRAMBLE_GLYPHS[(frame * 7 + index * 13) % SCRAMBLE_GLYPHS.length];\n          if (rendered[index] === glyph) return;\n          rendered[index] = glyph;\n          cell.setAttribute(\"d\", paths[glyph] ?? paths[\"?\"]);\n          cell.setAttribute(\"data-glyph\", glyph);\n        });\n      };\n      paintRef.current = (timestamp) => {\n        text = selectedCities.map((city) => {\n          const value =\n            timestamp === null || !city.formatter\n              ? `${showSeconds ? \"--:--:--\" : \"--:--\"}${hourFormat === \"12\" ? \" --\" : \"\"}`\n              : city.formatter.format(timestamp).toUpperCase();\n          return `${value}  ${city.displayLabel}`.padEnd(columns);\n        });\n        paint(true);\n      };\n      paint(true);\n      const media = gsap.matchMedia();\n      media.add(\n        {\n          reduced: \"(prefers-reduced-motion: reduce)\",\n          standard: \"(prefers-reduced-motion: no-preference)\",\n        },\n        (context) => {\n          const duration = bounded(scrambleDuration, 1.6, 0.3, 4);\n          const interval = bounded(scrambleInterval, 5, 0, 60);\n          const allowed = animated && !context.conditions?.reduced;\n          const cycle = gsap.timeline({\n            paused: true,\n            repeat: interval > 0 ? -1 : 0,\n          });\n          cycle.set(state, { progress: 0 }, 0);\n          cycle.to(\n            state,\n            {\n              progress: 1,\n              duration,\n              ease: \"power1.inOut\",\n              onUpdate: () => paint(),\n              onComplete: () => paint(true),\n            },\n            0,\n          );\n          if (interval > 0)\n            cycle.to({}, { duration: Math.max(0.8, interval - duration) });\n          const visible = () =>\n            !document.hidden && root.getAttribute(\"data-visible\") !== \"false\";\n          const syncPlayback = () => {\n            if (!allowed || !running || !visible()) {\n              cycle.pause(duration);\n              state.progress = 1;\n              paint(true);\n            } else {\n              cycle.play();\n            }\n          };\n          playbackRef.current = () => {\n            if (!allowed || !running || !visible()) return;\n            cycle.restart();\n          };\n          const observer = new IntersectionObserver(([entry]) => {\n            root.setAttribute(\"data-visible\", String(entry.isIntersecting));\n            syncPlayback();\n          });\n          observer.observe(root);\n          document.addEventListener(\"visibilitychange\", syncPlayback);\n          syncPlayback();\n          return () => {\n            observer.disconnect();\n            document.removeEventListener(\"visibilitychange\", syncPlayback);\n            playbackRef.current = null;\n            state.progress = 1;\n            paint(true);\n          };\n        },\n        root,\n      );\n      return () => {\n        media.revert();\n        paintRef.current = null;\n      };\n    },\n    {\n      scope: rootRef,\n      dependencies: [\n        selectedCities,\n        columns,\n        timeColumns,\n        paths,\n        showSeconds,\n        hourFormat,\n        animated,\n        running,\n        scrambleDuration,\n        scrambleInterval,\n      ],\n      revertOnUpdate: true,\n    },\n  );\n\n  React.useEffect(() => paintRef.current?.(time), [\n    time,\n    selectedCities,\n    paths,\n    running,\n    animated,\n    scrambleDuration,\n    scrambleInterval,\n    showSeconds,\n    hourFormat,\n  ]);\n  React.useEffect(() => {\n    if (replayKey !== 0) playbackRef.current?.();\n  }, [replayKey]);\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"matrix-clock\"\n      data-timestamp={time ?? undefined}\n      className={cn(\"w-full min-w-0\", className)}\n      style={{\n        maxWidth: bounded(maxWidth, 640, 180, 1200),\n        backgroundColor,\n        color,\n      }}\n      role=\"group\"\n      aria-label={ariaLabel}\n    >\n      <svg\n        viewBox={`0 0 ${columns * 6 - 1} ${height}`}\n        className=\"block h-auto w-full overflow-visible\"\n        aria-hidden=\"true\"\n        style={{\n          filter:\n            glow > 0\n              ? `drop-shadow(0 0 ${bounded(glow, 0.3, 0, 1.5)}px ${color})`\n              : undefined,\n        }}\n      >\n        {selectedCities.map((city, row) => (\n          <g\n            key={`${row}-${city.timeZone}`}\n            transform={`translate(0 ${row * pitch})`}\n            data-matrix-row={city.timeZone}\n          >\n            {Array.from({ length: columns }, (_, column) => (\n              <g key={column} transform={`translate(${column * 6} 0)`}>\n                <path\n                  d={inactivePath}\n                  fill=\"currentColor\"\n                  opacity={bounded(inactiveOpacity, 0.13, 0, 0.35)}\n                />\n                <path data-matrix-cell d=\"\" fill=\"currentColor\" />\n              </g>\n            ))}\n          </g>\n        ))}\n      </svg>\n      <ul className=\"sr-only\" aria-live=\"off\">\n        {selectedCities.map((city, index) => (\n          <li key={`${index}-${city.timeZone}`}>\n            {city.label}: <time>{displayTimes[index]}</time>\n            {!city.formatter && \" (Invalid time zone)\"}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\nexport default MatrixClock;\n",
      "type": "registry:ui",
      "target": "@ui/matrix-clock.tsx"
    },
    {
      "path": "src/registry/ui/use-time-source.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nconst timestamp = (value: Date | string | number | undefined) =>\n  value === undefined ? undefined : new Date(value).getTime();\n\nexport function useTimeSource({\n  mode,\n  duration,\n  targetDate,\n  date,\n  running,\n  onComplete,\n}: {\n  mode: \"clock\" | \"countdown\";\n  duration: number;\n  targetDate?: Date | string | number;\n  date?: Date | string | number;\n  running: boolean;\n  onComplete?: () => void;\n}) {\n  const [snapshot, setSnapshot] = React.useState<{\n    time: number | null;\n    remaining: number | null;\n  }>({ time: null, remaining: null });\n  const runRef = React.useRef({\n    signature: \"\",\n    remaining: 0,\n    deadline: 0,\n    completed: false,\n    clockTime: 0,\n  });\n  const completeRef = React.useRef(onComplete);\n  const seconds = bounded(duration, 3600, 0, 863999999);\n  const target = timestamp(targetDate);\n  const fixedDate = timestamp(date);\n\n  React.useEffect(() => {\n    completeRef.current = onComplete;\n  }, [onComplete]);\n\n  React.useEffect(() => {\n    const run = runRef.current;\n    const signature =\n      mode === \"countdown\"\n        ? `${mode}:${seconds}:${target}`\n        : `${mode}:${fixedDate}`;\n    if (run.signature !== signature) {\n      run.signature = signature;\n      run.remaining =\n        mode === \"countdown\" && target !== undefined && Number.isFinite(target)\n          ? Math.max(0, target - Date.now())\n          : seconds * 1000;\n      run.completed = false;\n      run.clockTime = 0;\n    }\n    const relative = mode === \"countdown\" && target === undefined;\n    if (relative && running) run.deadline = performance.now() + run.remaining;\n    let timer: ReturnType<typeof setTimeout> | undefined;\n    let disposed = false;\n\n    const refresh = () => {\n      if (disposed) return;\n      clearTimeout(timer);\n      const wallTime = Date.now();\n      let time: number | null = null;\n      let remaining: number | null = null;\n      if (mode === \"clock\") {\n        if (fixedDate !== undefined)\n          time = Number.isFinite(fixedDate) ? fixedDate : null;\n        else {\n          if (running || !run.clockTime)\n            run.clockTime = Math.floor(wallTime / 1000) * 1000;\n          time = run.clockTime;\n        }\n      } else if (relative) {\n        if (running)\n          run.remaining = Math.max(0, run.deadline - performance.now());\n        remaining = Math.ceil(run.remaining / 1000);\n      } else if (Number.isFinite(target)) {\n        if (running) run.remaining = Math.max(0, target! - wallTime);\n        remaining = Math.ceil(run.remaining / 1000);\n      }\n      setSnapshot((previous) =>\n        previous.time === time && previous.remaining === remaining\n          ? previous\n          : { time, remaining },\n      );\n      if (mode === \"countdown\" && remaining === 0 && !run.completed) {\n        run.completed = true;\n        completeRef.current?.();\n      }\n      const needsTick =\n        running &&\n        (mode === \"clock\"\n          ? fixedDate === undefined\n          : remaining !== null && remaining > 0);\n      if (needsTick && !document.hidden) {\n        const delay = relative\n          ? (run.remaining % 1000 || 1000) + 10\n          : 1010 - (Date.now() % 1000);\n        timer = setTimeout(refresh, delay);\n      }\n    };\n    const visibility = () => {\n      if (document.hidden) clearTimeout(timer);\n      else refresh();\n    };\n    refresh();\n    document.addEventListener(\"visibilitychange\", visibility);\n    return () => {\n      disposed = true;\n      clearTimeout(timer);\n      document.removeEventListener(\"visibilitychange\", visibility);\n      if (relative && running)\n        run.remaining = Math.max(0, run.deadline - performance.now());\n    };\n  }, [mode, seconds, target, fixedDate, running]);\n\n  return snapshot;\n}\n\n",
      "type": "registry:ui",
      "target": "@ui/use-time-source.ts"
    }
  ]
}
