{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-loading-states","type":"registry:component","title":"Agent Loading States","description":"Three loading states for AI interfaces: shimmering status text, live agent progress, and cycling reasoning phrases.","author":"Ryan","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/agent-loading-states.tsx","type":"registry:component","target":"@components/motion/agent-loading-states.tsx","content":"\"use client\";\n// easeui.dev/components/agents/agent-loading-states\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useState } from \"react\";\nimport { Progress } from \"@/components/motion/progress\";\nimport { cn } from \"@/lib/utils\";\n\nconst EASE = [0.23, 1, 0.32, 1] as const;\n\nexport interface ShimmerTextProps {\n  children: ReactNode;\n  className?: string;\n}\n\n/** Status text with a bright band sweeping across it, for a quiet \"still working\" signal. */\nexport function ShimmerText({ children, className }: ShimmerTextProps) {\n  const reduce = useReducedMotion();\n\n  if (reduce) {\n    return (\n      <span className={cn(\"text-sm font-medium text-muted-foreground\", className)}>\n        {children}\n      </span>\n    );\n  }\n\n  return (\n    <span\n      className={cn(\n        \"inline-block animate-shimmer bg-[length:200%_100%] bg-clip-text text-sm font-medium text-transparent [-webkit-background-clip:text]\",\n        className,\n      )}\n      style={{\n        backgroundImage:\n          \"linear-gradient(90deg, var(--muted-foreground) 30%, var(--foreground) 50%, var(--muted-foreground) 70%)\",\n      }}\n    >\n      {children}\n    </span>\n  );\n}\n\nexport interface AgentProgressProps {\n  label: string;\n  /** 0 to 100. Omitted, the bar runs indeterminate. */\n  value?: number;\n  className?: string;\n}\n\n/** A labeled progress bar with a pulsing dot marking it as a live, running step. */\nexport function AgentProgress({ label, value, className }: AgentProgressProps) {\n  return (\n    <div className={cn(\"flex flex-col gap-1.5\", className)}>\n      <span className=\"flex items-center gap-2 text-sm text-foreground\">\n        <span className=\"relative flex h-2 w-2 shrink-0\">\n          <span className=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-accent opacity-75 motion-reduce:hidden\" />\n          <span className=\"relative inline-flex h-2 w-2 rounded-full bg-accent\" />\n        </span>\n        {label}\n      </span>\n      <Progress value={value} />\n    </div>\n  );\n}\n\nexport interface ReasoningPhasesProps {\n  phases: string[];\n  /** Milliseconds each phrase stays before crossfading to the next. Default 2200. */\n  interval?: number;\n  className?: string;\n}\n\n/** Cycles through short phrases, crossfading between them, to narrate an agent's steps. */\nexport function ReasoningPhases({ phases, interval = 2200, className }: ReasoningPhasesProps) {\n  const [index, setIndex] = useState(0);\n  const reduce = useReducedMotion();\n\n  useEffect(() => {\n    if (phases.length < 2) return;\n    const timer = setInterval(() => setIndex((i) => (i + 1) % phases.length), interval);\n    return () => clearInterval(timer);\n  }, [phases.length, interval]);\n\n  return (\n    <div className={cn(\"text-sm text-muted-foreground\", className)}>\n      <AnimatePresence mode=\"wait\">\n        <motion.span\n          key={index}\n          initial={reduce ? false : { opacity: 0, y: 4 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0, y: -4, transition: { duration: reduce ? 0 : 0.1 } }}\n          transition={{ duration: reduce ? 0 : 0.2, ease: EASE }}\n          className=\"block\"\n        >\n          {phases[index]}\n        </motion.span>\n      </AnimatePresence>\n    </div>\n  );\n}\n"},{"path":"components/motion/progress.tsx","type":"registry:component","target":"@components/motion/progress.tsx","content":"import { cn } from \"@/lib/utils\";\n\nexport interface ProgressProps {\n  /** 0 to 100. Omitted, the bar runs indeterminate. */\n  value?: number;\n  /** Shows the percentage above the bar. Ignored when indeterminate. Default false. */\n  showValue?: boolean;\n  className?: string;\n}\n\n/** A progress bar that fills with scale, not width, so it stays cheap to animate. */\nexport function Progress({ value, showValue = false, className }: ProgressProps) {\n  const clamped = value === undefined ? undefined : Math.min(100, Math.max(0, value));\n\n  return (\n    <div className={cn(\"flex flex-col gap-1.5\", className)}>\n      {showValue && clamped !== undefined ? (\n        <span className=\"text-right text-xs tabular-nums text-muted-foreground\">\n          {Math.round(clamped)}%\n        </span>\n      ) : null}\n      <div\n        role=\"progressbar\"\n        aria-valuenow={clamped}\n        aria-valuemin={0}\n        aria-valuemax={100}\n        className=\"h-2 w-full overflow-hidden rounded-full bg-muted shadow-[0_0_0_1px_var(--border)]\"\n      >\n        <div\n          className={cn(\n            \"h-full w-full origin-left rounded-full bg-foreground\",\n            clamped === undefined\n              ? \"scale-x-[0.4] animate-progress-indeterminate motion-reduce:animate-none\"\n              : \"transition-transform duration-300 ease-out motion-reduce:transition-none\",\n          )}\n          style={clamped === undefined ? undefined : { transform: `scaleX(${clamped / 100})` }}\n        />\n      </div>\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"}]}