{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loading-state",
  "title": "Loading state",
  "description": "Pixel-grid AI loading state with Drive, Dots, and Orbit variants, a shimmer label, elapsed time, and reduced-motion support.",
  "registryDependencies": [
    "https://beadkit.dev/r/ai-core.json"
  ],
  "files": [
    {
      "path": "components/ai/LoadingState.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState, type CSSProperties } from \"react\";\nimport styles from \"./ai-state.module.css\";\n\nexport type LoadingStateVariant = \"drive\" | \"dots\" | \"orbit\";\n\nexport interface LoadingStateProps {\n  /** Short present-tense description of the work in progress. */\n  label?: string;\n  /** Motion pattern used by the 3 × 3 activity mark. */\n  variant?: LoadingStateVariant;\n  /** Show the live elapsed timer. */\n  showElapsed?: boolean;\n  /** Unix timestamp in milliseconds. Defaults to the component mount time. */\n  startedAt?: number;\n  className?: string;\n}\n\nconst chevron = Array.from({ length: 9 }, (_, index) => {\n  const row = Math.floor(index / 3);\n  const column = index % 3;\n  return (column + Math.abs(row - 1)) * 90;\n});\n\nconst orbitOrder = [0, 1, 2, 5, 8, 7, 6, 3];\nconst orbit = Array.from({ length: 9 }, (_, index) => {\n  const position = orbitOrder.indexOf(index);\n  return position === -1 ? null : position * 110;\n});\n\nconst patterns: Record<\n  LoadingStateVariant,\n  { delays: Array<number | null>; duration: number; round: boolean }\n> = {\n  drive: { delays: chevron, duration: 650, round: false },\n  dots: { delays: chevron, duration: 650, round: true },\n  orbit: { delays: orbit, duration: 950, round: false },\n};\n\nfunction formatElapsed(milliseconds: number) {\n  const total = Math.max(0, milliseconds) / 1000;\n  if (total < 60) return `${total.toFixed(1)}s`;\n  return `${Math.floor(total / 60)}m ${(total % 60).toFixed(1)}s`;\n}\n\nexport function LoadingState({\n  label = \"Working\",\n  variant = \"drive\",\n  showElapsed = true,\n  startedAt,\n  className = \"\",\n}: LoadingStateProps) {\n  const [mountedAt] = useState(() => Date.now());\n  const origin = startedAt ?? mountedAt;\n  const [elapsed, setElapsed] = useState(0);\n  const pattern = patterns[variant];\n\n  useEffect(() => {\n    const update = () => setElapsed(Date.now() - origin);\n    update();\n    const timer = window.setInterval(update, 100);\n    return () => window.clearInterval(timer);\n  }, [origin]);\n\n  return (\n    <div\n      data-ai-state=\"loading\"\n      data-variant={variant}\n      className={`inline-flex min-h-11 w-fit items-center gap-2.5 ${className}`}\n    >\n      <span\n        aria-hidden=\"true\"\n        className=\"grid grid-cols-[repeat(3,4px)] gap-[1.5px] text-foreground\"\n      >\n        {pattern.delays.map((delay, index) => (\n          <span\n            key={index}\n            className={`${styles.pixel} size-1 bg-current ${\n              pattern.round ? \"rounded-full\" : \"rounded-[1px]\"\n            }`}\n            style={\n              {\n                \"--beadkit-delay\": `${delay ?? 0}ms`,\n                \"--beadkit-cycle\": `${pattern.duration}ms`,\n                ...(delay === null ? { animation: \"none\", opacity: 0.07 } : {}),\n              } as CSSProperties\n            }\n          />\n        ))}\n      </span>\n      <span\n        role=\"status\"\n        aria-live=\"polite\"\n        data-label={label}\n        className={`${styles.shimmer} text-[13px] font-medium`}\n      >\n        {label}\n      </span>\n      {showElapsed ? (\n        <span aria-hidden=\"true\" className=\"text-muted-foreground font-mono text-xs tabular-nums\">\n          {formatElapsed(elapsed)}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai/LoadingState.tsx"
    }
  ],
  "type": "registry:component"
}
