{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pixel-loader","type":"registry:component","title":"Pixel Loader","description":"A 3x3 grid of cells that twinkle on independent, randomized cycles, paired with a shimmering label and an optional live elapsed timer.","author":"Ryan","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/pixel-loader.tsx","type":"registry:component","target":"@components/motion/pixel-loader.tsx","content":"\"use client\";\n// easeui.dev/components/agents/pixel-loader\n\nimport { useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst CELLS = 9;\nconst MIN_PULSE_MS = 500;\nconst MAX_PULSE_MS = 1100;\n\nfunction useElapsed(active: boolean) {\n  const [ms, setMs] = useState(0);\n  useEffect(() => {\n    if (!active) return;\n    const startedAt = Date.now();\n    const timer = setInterval(() => setMs(Date.now() - startedAt), 100);\n    return () => clearInterval(timer);\n  }, [active]);\n  const seconds = ms / 1000;\n  return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${(seconds % 60).toFixed(1)}s`;\n}\n\nexport interface PixelLoaderProps {\n  /** Status label shown beside the grid. Default \"Thinking\". */\n  label?: string;\n  /** Shows a live elapsed-time counter after the label. Default false. */\n  showElapsed?: boolean;\n  className?: string;\n}\n\n/**\n * A 3x3 grid of cells that twinkle on their own independent, randomized\n * cycles, for work that runs long enough to want a sense of progress. No\n * two cells share a rhythm, so nothing about it reads as a sweep or a\n * pattern. Pairs a shimmering label with an optional live timer. Reduced\n * motion holds the grid dim; the timer still ticks.\n */\nexport function PixelLoader({ label = \"Thinking\", showElapsed = false, className }: PixelLoaderProps) {\n  const cellRefs = useRef<(HTMLSpanElement | null)[]>([]);\n  const elapsed = useElapsed(showElapsed);\n  const reduce = useReducedMotion();\n\n  useEffect(() => {\n    if (reduce) return;\n    const animations = cellRefs.current.map((cell) =>\n      cell?.animate([{ opacity: 0.15 }, { opacity: 1 }, { opacity: 0.15 }], {\n        duration: MIN_PULSE_MS + Math.random() * (MAX_PULSE_MS - MIN_PULSE_MS),\n        delay: Math.random() * MAX_PULSE_MS,\n        iterations: Number.POSITIVE_INFINITY,\n        easing: \"ease-in-out\",\n      }),\n    );\n    return () => {\n      for (const animation of animations) animation?.cancel();\n    };\n  }, [reduce]);\n\n  return (\n    <div role=\"status\" className={cn(\"inline-flex items-center gap-2.5\", className)}>\n      <span aria-hidden=\"true\" className=\"grid grid-cols-3 gap-[3px]\">\n        {Array.from({ length: CELLS }, (_, index) => (\n          <span\n            // biome-ignore lint/suspicious/noArrayIndexKey: a fixed 3x3 grid, cells never reorder.\n            key={index}\n            ref={(el) => {\n              cellRefs.current[index] = el;\n            }}\n            className=\"h-[5px] w-[5px] rounded-[1px] bg-foreground\"\n            style={{ opacity: reduce ? 0.5 : 0.15 }}\n          />\n        ))}\n      </span>\n      <span\n        className={cn(\n          \"inline-block text-sm font-medium\",\n          reduce\n            ? \"text-muted-foreground\"\n            : \"animate-shimmer bg-[length:200%_100%] bg-clip-text text-transparent [-webkit-background-clip:text]\",\n        )}\n        style={\n          reduce\n            ? undefined\n            : {\n                backgroundImage:\n                  \"linear-gradient(90deg, var(--muted-foreground) 30%, var(--foreground) 50%, var(--muted-foreground) 70%)\",\n              }\n        }\n      >\n        {label}\n      </span>\n      {showElapsed ? (\n        <span className=\"font-mono text-xs text-muted-foreground tabular-nums\">{elapsed}</span>\n      ) : null}\n    </div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}