{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"An ArcadePixel component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useRef } from \"react\";\n\n/**\n * 5x7 pixel font glyphs expressed as readable row strings (\"#\" = lit pixel,\n * \".\" = blank pixel). Converted to numeric grids in PIXEL_FONT_5X7 below.\n */\nconst FONT_GLYPH_ROWS: Record<string, string[]> = {\n  \" \": [\".....\", \".....\", \".....\", \".....\", \".....\", \".....\", \".....\"],\n  \"-\": [\".....\", \".....\", \".....\", \"#####\", \".....\", \".....\", \".....\"],\n  \",\": [\".....\", \".....\", \".....\", \".....\", \"..#..\", \"..#..\", \".#...\"],\n  \":\": [\".....\", \"..#..\", \"..#..\", \".....\", \"..#..\", \"..#..\", \".....\"],\n  \"!\": [\"..#..\", \"..#..\", \"..#..\", \"..#..\", \"..#..\", \".....\", \"..#..\"],\n  \"?\": [\".###.\", \"#...#\", \"....#\", \"...#.\", \"..#..\", \".....\", \"..#..\"],\n  \".\": [\".....\", \".....\", \".....\", \".....\", \".....\", \"..#..\", \"..#..\"],\n  \"*\": [\".....\", \"#.#.#\", \".###.\", \"#####\", \".###.\", \"#.#.#\", \".....\"],\n  \"0\": [\".###.\", \"#...#\", \"#..##\", \"#.#.#\", \"##..#\", \"#...#\", \".###.\"],\n  \"1\": [\"..#..\", \".##..\", \"..#..\", \"..#..\", \"..#..\", \"..#..\", \".###.\"],\n  \"2\": [\".###.\", \"#...#\", \"....#\", \"...#.\", \"..#..\", \".#...\", \"#####\"],\n  \"3\": [\".###.\", \"#...#\", \"....#\", \"..##.\", \"....#\", \"#...#\", \".###.\"],\n  \"4\": [\"#...#\", \"#...#\", \"#...#\", \"#####\", \"....#\", \"....#\", \"....#\"],\n  \"5\": [\"#####\", \"#....\", \"#....\", \"####.\", \"....#\", \"....#\", \"####.\"],\n  \"6\": [\".###.\", \"#....\", \"#....\", \"####.\", \"#...#\", \"#...#\", \".###.\"],\n  \"7\": [\"#####\", \"....#\", \"...#.\", \"..#..\", \"..#..\", \"..#..\", \"..#..\"],\n  \"8\": [\".###.\", \"#...#\", \"#...#\", \".###.\", \"#...#\", \"#...#\", \".###.\"],\n  \"9\": [\".###.\", \"#...#\", \"#...#\", \".####\", \"....#\", \"#...#\", \".###.\"],\n  A: [\".###.\", \"#...#\", \"#...#\", \"#####\", \"#...#\", \"#...#\", \"#...#\"],\n  B: [\"####.\", \"#...#\", \"#...#\", \"####.\", \"#...#\", \"#...#\", \"####.\"],\n  C: [\".####\", \"#....\", \"#....\", \"#....\", \"#....\", \"#....\", \".####\"],\n  D: [\"####.\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \"####.\"],\n  E: [\"#####\", \"#....\", \"#....\", \"####.\", \"#....\", \"#....\", \"#####\"],\n  F: [\"#####\", \"#....\", \"#....\", \"####.\", \"#....\", \"#....\", \"#....\"],\n  G: [\".####\", \"#....\", \"#....\", \"#..##\", \"#...#\", \"#...#\", \".####\"],\n  H: [\"#...#\", \"#...#\", \"#...#\", \"#####\", \"#...#\", \"#...#\", \"#...#\"],\n  I: [\"#####\", \"..#..\", \"..#..\", \"..#..\", \"..#..\", \"..#..\", \"#####\"],\n  J: [\"..###\", \"...#.\", \"...#.\", \"...#.\", \"...#.\", \"#..#.\", \".##..\"],\n  K: [\"#...#\", \"#..#.\", \"#.#..\", \"##...\", \"#.#..\", \"#..#.\", \"#...#\"],\n  L: [\"#....\", \"#....\", \"#....\", \"#....\", \"#....\", \"#....\", \"#####\"],\n  M: [\"#...#\", \"##.##\", \"#.#.#\", \"#.#.#\", \"#...#\", \"#...#\", \"#...#\"],\n  N: [\"#...#\", \"##..#\", \"#.#.#\", \"#.#.#\", \"#..##\", \"#...#\", \"#...#\"],\n  O: [\".###.\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \".###.\"],\n  P: [\"####.\", \"#...#\", \"#...#\", \"####.\", \"#....\", \"#....\", \"#....\"],\n  Q: [\".###.\", \"#...#\", \"#...#\", \"#...#\", \"#.#.#\", \"#..#.\", \".##.#\"],\n  R: [\"####.\", \"#...#\", \"#...#\", \"####.\", \"#.#..\", \"#..#.\", \"#...#\"],\n  S: [\".####\", \"#....\", \"#....\", \".###.\", \"....#\", \"....#\", \"####.\"],\n  T: [\"#####\", \"..#..\", \"..#..\", \"..#..\", \"..#..\", \"..#..\", \"..#..\"],\n  U: [\"#...#\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \".###.\"],\n  V: [\"#...#\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \".#.#.\", \"..#..\"],\n  W: [\"#...#\", \"#...#\", \"#...#\", \"#.#.#\", \"#.#.#\", \"#.#.#\", \".#.#.\"],\n  X: [\"#...#\", \"#...#\", \".#.#.\", \"..#..\", \".#.#.\", \"#...#\", \"#...#\"],\n  Y: [\"#...#\", \"#...#\", \".#.#.\", \"..#..\", \"..#..\", \"..#..\", \"..#..\"],\n  Z: [\"#####\", \"....#\", \"...#.\", \"..#..\", \".#...\", \"#....\", \"#####\"],\n};\n\n/** Glyph grid dimensions. */\nconst FONT_COLUMNS = 5;\nconst FONT_ROWS = 7;\n/** Blank columns inserted between characters when building a text grid. */\nconst CHARACTER_SPACING_COLUMNS = 1;\nconst CHARACTER_UNIT_WIDTH = FONT_COLUMNS + CHARACTER_SPACING_COLUMNS;\n\nconst BLANK_CELL_CHARACTERS = new Set([\".\", \" \"]);\n\n/**\n * Row strings accept \".\" or \" \" for an unlit cell, \"#\" for palette index 1, and\n * a digit for any other palette index, so sprites can be multi-colour.\n */\nconst characterToCellValue = (character: string): number => {\n  if (BLANK_CELL_CHARACTERS.has(character)) {\n    return 0;\n  }\n  const parsed = Number.parseInt(character, 10);\n  return Number.isNaN(parsed) ? 1 : parsed;\n};\n\nconst rowsToGrid = (rows: string[]): number[][] =>\n  rows.map((row) => Array.from(row, characterToCellValue));\n\n/** Numeric 5x7 pixel font map (0 = blank, 1 = lit), keyed by uppercase character. */\nexport const PIXEL_FONT_5X7: Record<string, number[][]> = Object.fromEntries(\n  Object.entries(FONT_GLYPH_ROWS).map(([character, rows]) => [\n    character,\n    rowsToGrid(rows),\n  ])\n);\n\n/**\n * Ready-made sprites so the `sprite` path is usable without hand-authoring a\n * grid. `alien` and `alienAlt` are the two frames of a space-invader style\n * marcher: alternate them on a timer to animate it.\n */\nexport const ARCADE_SPRITES: Record<string, number[][]> = {\n  alien: rowsToGrid([\n    \"..#.....#..\",\n    \"...#...#...\",\n    \"..#######..\",\n    \".##.###.##.\",\n    \"###########\",\n    \"#.#######.#\",\n    \"#.#.....#.#\",\n    \"...##.##...\",\n  ]),\n  alienAlt: rowsToGrid([\n    \"..#.....#..\",\n    \"#..#...#..#\",\n    \"#.#######.#\",\n    \"###.###.###\",\n    \"###########\",\n    \".#########.\",\n    \"..#.....#..\",\n    \".##.....##.\",\n  ]),\n  arrow: rowsToGrid([\n    \"....#....\",\n    \"....##...\",\n    \"....###..\",\n    \"########.\",\n    \"#########\",\n    \"########.\",\n    \"....###..\",\n    \"....##...\",\n    \"....#....\",\n  ]),\n  heart: rowsToGrid([\n    \".###.###.\",\n    \"#22######\",\n    \"#2#######\",\n    \"#########\",\n    \".#######.\",\n    \"..#####..\",\n    \"...###...\",\n    \"....#....\",\n  ]),\n};\n\nconst BLANK_GLYPH_GRID = rowsToGrid(\n  Array.from({ length: FONT_ROWS }, () => \".\".repeat(FONT_COLUMNS))\n);\n\nconst getGlyphGrid = (character: string): number[][] =>\n  PIXEL_FONT_5X7[character.toUpperCase()] ?? BLANK_GLYPH_GRID;\n\nconst buildTextGrid = (text: string): number[][] => {\n  const characters = text.length > 0 ? Array.from(text) : [\" \"];\n  const rows: number[][] = Array.from(\n    { length: FONT_ROWS },\n    () => [] as number[]\n  );\n\n  for (const [index, character] of characters.entries()) {\n    const glyph = getGlyphGrid(character);\n    for (let rowIndex = 0; rowIndex < FONT_ROWS; rowIndex++) {\n      rows[rowIndex].push(...glyph[rowIndex]);\n      if (index < characters.length - 1) {\n        rows[rowIndex].push(0);\n      }\n    }\n  }\n\n  return rows;\n};\n\nconst resolveGrid = (sprite?: number[][], text?: string): number[][] => {\n  if (sprite && sprite.length > 0) {\n    return sprite;\n  }\n  if (typeof text === \"string\") {\n    return buildTextGrid(text);\n  }\n  return BLANK_GLYPH_GRID;\n};\n\n/**\n * `fillStyle` cannot resolve CSS custom properties, so `var(--token)` palette\n * entries are read off the live element before the first paint.\n */\nconst CSS_VARIABLE_PATTERN = /^var\\(\\s*(--[\\w-]+)\\s*(?:,\\s*([\\s\\S]+))?\\)$/;\nconst FALLBACK_COLOR = \"#ffffff\";\n\nconst resolveColor = (element: HTMLElement, color: string): string => {\n  const match = CSS_VARIABLE_PATTERN.exec(color.trim());\n  if (!match) {\n    return color;\n  }\n  const computed = getComputedStyle(element).getPropertyValue(match[1]).trim();\n  if (computed) {\n    return computed;\n  }\n  return match[2] ? resolveColor(element, match[2].trim()) : FALLBACK_COLOR;\n};\n\n/**\n * Palettes arrive as arrays, which are a fresh reference on every render when\n * written inline. Serialising them gives the render effect a stable dependency.\n * The separator is a unit separator rather than a space, because a modern colour\n * such as `oklch(0.72 0.2 145)` contains spaces of its own.\n */\nconst PALETTE_SEPARATOR = \"\\u001f\";\n\ntype FrameState = {\n  done: boolean;\n  /** Column range currently landing with a type-reveal flash. */\n  flashEnd: number;\n  flashStart: number;\n  offsetColumns: number;\n  revealedColumns: number;\n  visible: boolean;\n};\n\ntype PaintOptions = {\n  bloomAlpha: number;\n  columnsInView: number;\n  frame: FrameState;\n  grid: number[][];\n  height: number;\n  matrixColor: string;\n  palette: string[];\n  /** Column span after which the grid repeats, for a seamless marquee. */\n  period: number;\n  pixelSize: number;\n  stride: number;\n  width: number;\n};\n\nconst isSameFrame = (a: FrameState, b: FrameState): boolean =>\n  a.visible === b.visible &&\n  a.revealedColumns === b.revealedColumns &&\n  a.offsetColumns === b.offsetColumns &&\n  a.flashStart === b.flashStart &&\n  a.flashEnd === b.flashEnd;\n\nconst FLASH_COLOR = \"#ffffff\";\nconst BLOOM_BLUR_PX = 5;\nconst MATRIX_ALPHA = 0.07;\n\n/** Crisp, opaque pixels: one fillRect per lit cell. */\nconst paintLitPixels = (\n  ctx: CanvasRenderingContext2D,\n  options: PaintOptions\n): void => {\n  const { frame, period, pixelSize, stride, width } = options;\n  const repeats = period > 0 ? [0, period] : [0];\n\n  for (const [rowIndex, row] of options.grid.entries()) {\n    const y = rowIndex * stride;\n    for (const [columnIndex, value] of row.entries()) {\n      if (!value || columnIndex >= frame.revealedColumns) {\n        continue;\n      }\n      const isFlashing =\n        columnIndex >= frame.flashStart && columnIndex < frame.flashEnd;\n      const color = isFlashing\n        ? FLASH_COLOR\n        : (options.palette[value] ?? options.palette.at(-1));\n      if (!color || color === \"transparent\") {\n        continue;\n      }\n      ctx.fillStyle = color;\n      for (const repeat of repeats) {\n        const x = (columnIndex + repeat - frame.offsetColumns) * stride;\n        if (x + pixelSize < 0 || x > width) {\n          continue;\n        }\n        ctx.fillRect(x, y, pixelSize, pixelSize);\n      }\n    }\n  }\n};\n\n/**\n * Phosphor bleed. One blurred self-composite in `lighter` mode costs a single\n * draw call, where a per-pixel `shadowBlur` would cost one blur per lit cell.\n */\nconst paintBloom = (\n  ctx: CanvasRenderingContext2D,\n  canvas: HTMLCanvasElement,\n  options: PaintOptions\n): void => {\n  if (options.bloomAlpha <= 0) {\n    return;\n  }\n  ctx.save();\n  ctx.globalCompositeOperation = \"lighter\";\n  ctx.globalAlpha = options.bloomAlpha;\n  ctx.filter = `blur(${BLOOM_BLUR_PX}px)`;\n  ctx.drawImage(canvas, 0, 0, options.width, options.height);\n  ctx.restore();\n};\n\n/**\n * The unlit dot matrix behind the message. Drawn last with `destination-over`\n * so it never dulls a lit pixel, and phase-locked to the marquee offset so the\n * dots and the message always sit on the same sub-cell grid.\n */\nconst paintDotMatrix = (\n  ctx: CanvasRenderingContext2D,\n  options: PaintOptions\n): void => {\n  const { columnsInView, frame, pixelSize, stride } = options;\n  const phase = frame.offsetColumns - Math.floor(frame.offsetColumns);\n  ctx.save();\n  ctx.globalCompositeOperation = \"destination-over\";\n  ctx.globalAlpha = MATRIX_ALPHA;\n  ctx.fillStyle = options.matrixColor;\n  for (let rowIndex = 0; rowIndex < options.grid.length; rowIndex++) {\n    const y = rowIndex * stride;\n    for (let column = 0; column <= columnsInView; column++) {\n      ctx.fillRect((column - phase) * stride, y, pixelSize, pixelSize);\n    }\n  }\n  ctx.restore();\n};\n\nconst paintFrame = (\n  ctx: CanvasRenderingContext2D,\n  canvas: HTMLCanvasElement,\n  options: PaintOptions\n): void => {\n  ctx.clearRect(0, 0, options.width, options.height);\n  if (options.frame.visible) {\n    paintLitPixels(ctx, options);\n    paintBloom(ctx, canvas, options);\n  }\n  paintDotMatrix(ctx, options);\n};\n\nexport type ArcadePixelAnimate = \"none\" | \"type\" | \"wipe\" | \"blink\" | \"marquee\";\n\nexport interface ArcadePixelProps {\n  /** Reveal animation played on mount. `marquee` scrolls the message forever. */\n  animate?: ArcadePixelAnimate;\n  className?: string;\n  /** Fixed display width in pixel cells. Defaults to the content width. */\n  columns?: number;\n  /** Curved vignette, chromatic fringing and a faint flicker over the canvas. */\n  crt?: boolean;\n  /** Gap, in CSS pixels, between pixel cells. */\n  gap?: number;\n  /** Phosphor bloom bleeding out of lit pixels. A number sets its intensity (0-1). */\n  glow?: boolean | number;\n  /** Whether the reveal animation repeats indefinitely. */\n  loop?: boolean;\n  /** Colors indexed by sprite/font value. Index 0 is treated as background. */\n  palette?: string[];\n  /** Size, in CSS pixels, of a single pixel cell. */\n  pixelSize?: number;\n  /** Overlays a CRT-style scanline pattern. */\n  scanlines?: boolean;\n  /** Animation speed multiplier. Higher plays faster. */\n  speed?: number;\n  /** 2D array of palette indices. Takes precedence over `text` when both are set. */\n  sprite?: number[][];\n  /** Text rendered through the built-in 5x7 pixel font. Ignored if `sprite` is set. */\n  text?: string;\n}\n\nconst DEFAULT_PIXEL_SIZE = 8;\nconst DEFAULT_GAP = 1;\nconst DEFAULT_PALETTE: string[] = [\"transparent\", \"var(--color-brand)\"];\nconst DEFAULT_SPEED = 1;\nconst MAX_DEVICE_PIXEL_RATIO = 2;\nconst TYPE_DURATION_MS = 1400;\nconst WIPE_DURATION_MS = 900;\nconst BLINK_INTERVAL_MS = 450;\nconst BLINK_SETTLE_CYCLES = 3;\nconst TYPE_FLASH_MS = 110;\nconst MARQUEE_COLUMNS_PER_SECOND = 16;\nconst MARQUEE_TAIL_COLUMNS = 8;\nconst DEFAULT_GLOW_INTENSITY = 0.65;\nconst MS_PER_SECOND = 1000;\n\n/** Linear, expressed as a cubic-bezier so no string easing reaches Motion. */\nconst LINEAR_EASE: [number, number, number, number] = [0, 0, 1, 1];\nconst SCANLINE_PERIOD_PX = 3;\nconst SCANLINE_DRIFT_PX = SCANLINE_PERIOD_PX * 2;\nconst SCANLINE_DRIFT_SECONDS = 5;\nconst ROLL_BAND_SECONDS = 9;\nconst FLICKER_SECONDS = 2.4;\n\nconst SCANLINE_BACKGROUND = `repeating-linear-gradient(to bottom, rgba(0,0,0,0.44) 0px, rgba(0,0,0,0.44) 1px, rgba(0,0,0,0) 1px, rgba(0,0,0,0) ${SCANLINE_PERIOD_PX}px)`;\nconst ROLL_BAND_BACKGROUND =\n  \"linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,0.05) 50%, rgba(255,255,255,0) 100%)\";\nconst ABERRATION_BACKGROUND =\n  \"linear-gradient(90deg, rgba(255,0,72,0.18) 0%, rgba(255,0,72,0) 13%, rgba(0,190,255,0) 87%, rgba(0,190,255,0.18) 100%)\";\nconst VIGNETTE_BACKGROUND =\n  \"radial-gradient(125% 125% at 50% 50%, rgba(0,0,0,0) 40%, rgba(0,0,0,0.32) 74%, rgba(0,0,0,0.82) 100%)\";\n\ntype ArcadeCrtOverlayProps = {\n  crt: boolean;\n  scanlines: boolean;\n  shouldReduceMotion: boolean;\n};\n\n/**\n * Every CRT artefact is a CSS layer stacked over the canvas, so none of it costs\n * per-frame canvas work.\n */\nconst ArcadeCrtOverlay = ({\n  crt,\n  scanlines,\n  shouldReduceMotion,\n}: ArcadeCrtOverlayProps) => (\n  <>\n    {scanlines ? (\n      <motion.div\n        animate={\n          shouldReduceMotion\n            ? undefined\n            : {\n                transform: [\n                  \"translateY(0px)\",\n                  `translateY(-${SCANLINE_DRIFT_PX}px)`,\n                ],\n              }\n        }\n        className=\"absolute inset-x-0 top-0 h-[200%]\"\n        style={{ backgroundImage: SCANLINE_BACKGROUND }}\n        transition={{\n          duration: SCANLINE_DRIFT_SECONDS,\n          ease: LINEAR_EASE,\n          repeat: Number.POSITIVE_INFINITY,\n        }}\n      />\n    ) : null}\n    {crt ? (\n      <>\n        {shouldReduceMotion ? null : (\n          <motion.div\n            animate={{ transform: [\"translateY(-140%)\", \"translateY(240%)\"] }}\n            className=\"absolute inset-x-0 top-0 h-1/3\"\n            style={{ backgroundImage: ROLL_BAND_BACKGROUND }}\n            transition={{\n              duration: ROLL_BAND_SECONDS,\n              ease: LINEAR_EASE,\n              repeat: Number.POSITIVE_INFINITY,\n            }}\n          />\n        )}\n        <div\n          className=\"absolute inset-0\"\n          style={{\n            backgroundImage: ABERRATION_BACKGROUND,\n            mixBlendMode: \"screen\",\n          }}\n        />\n        <div\n          className=\"absolute inset-0\"\n          style={{\n            backgroundImage: VIGNETTE_BACKGROUND,\n            boxShadow: \"inset 0 0 24px rgba(0,0,0,0.65)\",\n          }}\n        />\n        {shouldReduceMotion ? null : (\n          <motion.div\n            animate={{ opacity: [0.055, 0.015, 0.045, 0.01, 0.06] }}\n            className=\"absolute inset-0 bg-white\"\n            transition={{\n              duration: FLICKER_SECONDS,\n              ease: LINEAR_EASE,\n              repeat: Number.POSITIVE_INFINITY,\n              repeatType: \"mirror\",\n            }}\n          />\n        )}\n      </>\n    ) : null}\n  </>\n);\n\nconst ArcadePixel = ({\n  sprite,\n  text,\n  palette = DEFAULT_PALETTE,\n  columns,\n  pixelSize = DEFAULT_PIXEL_SIZE,\n  gap = DEFAULT_GAP,\n  scanlines = false,\n  crt = false,\n  glow = false,\n  animate = \"none\",\n  speed = DEFAULT_SPEED,\n  loop = false,\n  className,\n}: ArcadePixelProps) => {\n  const shouldReduceMotion = Boolean(useReducedMotion());\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  const grid = useMemo(() => resolveGrid(sprite, text), [sprite, text]);\n  const paletteKey = palette.join(PALETTE_SEPARATOR);\n  const stablePalette = useMemo(\n    () => paletteKey.split(PALETTE_SEPARATOR),\n    [paletteKey]\n  );\n\n  const totalRows = grid.length;\n  const totalColumns = grid[0]?.length ?? 0;\n  const columnsInView = Math.max(1, columns ?? totalColumns);\n  const stride = pixelSize + gap;\n  const cssWidth = columnsInView * stride - gap;\n  const cssHeight = totalRows * stride - gap;\n\n  const { numUnits, unitWidth } = useMemo(() => {\n    if (!sprite && typeof text === \"string\") {\n      return {\n        numUnits: Math.max(1, Array.from(text).length),\n        unitWidth: CHARACTER_UNIT_WIDTH,\n      };\n    }\n    return { numUnits: Math.max(1, totalColumns), unitWidth: 1 };\n  }, [sprite, text, totalColumns]);\n\n  const accessibleLabel =\n    typeof text === \"string\" && text.trim().length > 0 ? text : \"pixel art\";\n\n  const glowIntensity =\n    typeof glow === \"number\" ? glow : (glow && DEFAULT_GLOW_INTENSITY) || 0;\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!(container && canvas && ctx)) {\n      return;\n    }\n\n    const safeSpeed = speed > 0 ? speed : DEFAULT_SPEED;\n    const isStatic = shouldReduceMotion || animate === \"none\";\n    const resolvedPalette = stablePalette.map((color) =>\n      color === \"transparent\" ? color : resolveColor(container, color)\n    );\n    const matrixColor =\n      resolvedPalette.find((color) => color !== \"transparent\") ??\n      FALLBACK_COLOR;\n    const period =\n      animate === \"marquee\" ? totalColumns + MARQUEE_TAIL_COLUMNS : 0;\n\n    const staticFrame: FrameState = {\n      done: true,\n      flashEnd: -1,\n      flashStart: -1,\n      offsetColumns: 0,\n      revealedColumns: totalColumns,\n      visible: true,\n    };\n\n    let rafId: number | null = null;\n    let isFinished = isStatic;\n    let isOffscreen = false;\n    let isHidden = document.hidden;\n    let isPaused = false;\n    let elapsedMs = 0;\n    let lastTimestamp: number | null = null;\n    let lastFrame: FrameState = staticFrame;\n    let currentCssWidth = cssWidth;\n    let currentCssHeight = cssHeight;\n    let landedUnits = 0;\n    let flashStartedAtMs = Number.NEGATIVE_INFINITY;\n\n    const setBackingStoreSize = () => {\n      const rect = container.getBoundingClientRect();\n      currentCssWidth = rect.width;\n      currentCssHeight = rect.height;\n      const devicePixelRatio = Math.min(\n        window.devicePixelRatio || 1,\n        MAX_DEVICE_PIXEL_RATIO\n      );\n      const width = Math.max(1, Math.round(rect.width * devicePixelRatio));\n      const height = Math.max(1, Math.round(rect.height * devicePixelRatio));\n      if (canvas.width !== width || canvas.height !== height) {\n        canvas.width = width;\n        canvas.height = height;\n      }\n      ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);\n    };\n\n    const render = (frame: FrameState) => {\n      lastFrame = frame;\n      paintFrame(ctx, canvas, {\n        bloomAlpha: glowIntensity,\n        columnsInView,\n        frame,\n        grid,\n        height: currentCssHeight,\n        matrixColor,\n        palette: resolvedPalette,\n        period,\n        pixelSize,\n        stride,\n        width: currentCssWidth,\n      });\n    };\n\n    const computeTypeFrame = (): FrameState => {\n      const progress = Math.min(elapsedMs / (TYPE_DURATION_MS / safeSpeed), 1);\n      const units = Math.floor(progress * numUnits);\n      if (units !== landedUnits) {\n        landedUnits = units;\n        flashStartedAtMs = elapsedMs;\n      }\n      const isFlashing = elapsedMs - flashStartedAtMs < TYPE_FLASH_MS;\n      return {\n        done: progress >= 1,\n        flashEnd: isFlashing ? units * unitWidth : -1,\n        flashStart: isFlashing ? (units - 1) * unitWidth : -1,\n        offsetColumns: 0,\n        revealedColumns: Math.min(totalColumns, units * unitWidth),\n        visible: true,\n      };\n    };\n\n    const computeWipeFrame = (): FrameState => {\n      const progress = Math.min(elapsedMs / (WIPE_DURATION_MS / safeSpeed), 1);\n      return {\n        ...staticFrame,\n        done: progress >= 1,\n        revealedColumns: Math.floor(progress * totalColumns),\n      };\n    };\n\n    const computeBlinkFrame = (): FrameState => {\n      const phase = Math.floor(elapsedMs / (BLINK_INTERVAL_MS / safeSpeed));\n      const settled = !loop && phase >= BLINK_SETTLE_CYCLES * 2;\n      return {\n        ...staticFrame,\n        done: settled,\n        visible: settled || phase % 2 === 0,\n      };\n    };\n\n    const computeMarqueeFrame = (): FrameState => {\n      const columnsPerMs =\n        (MARQUEE_COLUMNS_PER_SECOND * safeSpeed) / MS_PER_SECOND;\n      return {\n        ...staticFrame,\n        done: false,\n        offsetColumns: (elapsedMs * columnsPerMs) % Math.max(1, period),\n      };\n    };\n\n    const computeFrame = (): FrameState => {\n      if (animate === \"type\") {\n        return computeTypeFrame();\n      }\n      if (animate === \"wipe\") {\n        return computeWipeFrame();\n      }\n      if (animate === \"blink\") {\n        return computeBlinkFrame();\n      }\n      if (animate === \"marquee\") {\n        return computeMarqueeFrame();\n      }\n      return staticFrame;\n    };\n\n    const step = (timestamp: number) => {\n      rafId = null;\n      if (isPaused || isFinished) {\n        return;\n      }\n      if (lastTimestamp !== null) {\n        elapsedMs += timestamp - lastTimestamp;\n      }\n      lastTimestamp = timestamp;\n\n      const frame = computeFrame();\n      // A blinking display only changes twice a second, so repainting it every\n      // rAF tick would be pure waste.\n      if (!isSameFrame(frame, lastFrame)) {\n        render(frame);\n      }\n\n      if (frame.done) {\n        if (loop) {\n          elapsedMs = 0;\n          landedUnits = 0;\n          flashStartedAtMs = Number.NEGATIVE_INFINITY;\n        } else {\n          render(staticFrame);\n          isFinished = true;\n          return;\n        }\n      }\n      rafId = requestAnimationFrame(step);\n    };\n\n    const schedule = () => {\n      if (isPaused || isFinished || rafId !== null) {\n        return;\n      }\n      rafId = requestAnimationFrame(step);\n    };\n\n    const updatePauseState = () => {\n      const shouldPause = isOffscreen || isHidden;\n      if (shouldPause === isPaused) {\n        return;\n      }\n      isPaused = shouldPause;\n      if (isPaused) {\n        if (rafId !== null) {\n          cancelAnimationFrame(rafId);\n          rafId = null;\n        }\n        lastTimestamp = null;\n      } else {\n        schedule();\n      }\n    };\n\n    setBackingStoreSize();\n    if (isFinished) {\n      render(staticFrame);\n    } else {\n      render({ ...staticFrame, revealedColumns: 0 });\n      schedule();\n    }\n\n    const resizeObserver = new ResizeObserver(() => {\n      setBackingStoreSize();\n      render(lastFrame);\n    });\n    resizeObserver.observe(container);\n\n    const intersectionObserver = new IntersectionObserver((entries) => {\n      const [entry] = entries;\n      if (!entry) {\n        return;\n      }\n      isOffscreen = !entry.isIntersecting;\n      updatePauseState();\n    });\n    intersectionObserver.observe(container);\n\n    const handleVisibilityChange = () => {\n      isHidden = document.hidden;\n      updatePauseState();\n    };\n    document.addEventListener(\"visibilitychange\", handleVisibilityChange);\n\n    return () => {\n      if (rafId !== null) {\n        cancelAnimationFrame(rafId);\n      }\n      resizeObserver.disconnect();\n      intersectionObserver.disconnect();\n      document.removeEventListener(\"visibilitychange\", handleVisibilityChange);\n    };\n  }, [\n    grid,\n    stablePalette,\n    pixelSize,\n    stride,\n    columnsInView,\n    glowIntensity,\n    animate,\n    speed,\n    loop,\n    shouldReduceMotion,\n    totalColumns,\n    numUnits,\n    unitWidth,\n    cssWidth,\n    cssHeight,\n  ]);\n\n  return (\n    <div\n      className={cn(\"relative inline-block overflow-hidden\", className)}\n      ref={containerRef}\n      style={{ height: cssHeight, width: cssWidth }}\n    >\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 select-none\"\n      >\n        <canvas className=\"block h-full w-full\" ref={canvasRef} />\n        <ArcadeCrtOverlay\n          crt={crt}\n          scanlines={scanlines}\n          shouldReduceMotion={shouldReduceMotion}\n        />\n      </div>\n      <span className=\"sr-only\">{accessibleLabel}</span>\n    </div>\n  );\n};\n\nexport default ArcadePixel;\n","path":"index.tsx","target":"components/smoothui/arcade-pixel/index.tsx","type":"registry:ui"}],"name":"arcade-pixel","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Arcade Pixel","type":"registry:ui"}