{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "status-pill",
  "type": "registry:component",
  "title": "Status Pill",
  "description": "A tiny working indicator that rolls through what it's doing, from Reading to Searching to Writing, eases to fit each word and lands on a green check when it's finished.",
  "categories": [
    "component",
    "Conversation"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/status-pill.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface StatusPillProps {\n  phases?: string[];\n  doneLabel?: string;\n  interval?: number;\n  done?: boolean;\n  loop?: boolean;\n  size?: \"sm\" | \"default\";\n  className?: string;\n}\n\nconst spring = { type: \"spring\", stiffness: 420, damping: 34 } as const;\nconst easeOut = [0.23, 1, 0.32, 1] as const;\n\nfunction useMountedReducedMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nexport function StatusPill({\n  phases = [\"Reading\", \"Searching\", \"Writing\"],\n  doneLabel = \"Done\",\n  interval = 1400,\n  done: doneProp,\n  loop = false,\n  size = \"default\",\n  className,\n}: StatusPillProps) {\n  const reduce = useMountedReducedMotion();\n  const [step, setStep] = useState(0);\n  const controlled = doneProp !== undefined;\n  const done = controlled ? doneProp : step >= phases.length;\n\n  useEffect(() => {\n    if (controlled) {\n      if (doneProp) return;\n      const t = setTimeout(() => setStep((s) => (s + 1) % phases.length), interval);\n      return () => clearTimeout(t);\n    }\n    if (step >= phases.length) {\n      if (!loop) return;\n      const t = setTimeout(() => setStep(0), interval * 1.8);\n      return () => clearTimeout(t);\n    }\n    const t = setTimeout(() => setStep((s) => s + 1), interval);\n    return () => clearTimeout(t);\n  }, [controlled, doneProp, step, phases.length, interval, loop]);\n\n  const label = done ? doneLabel : (phases[step % phases.length] ?? \"\");\n  const text = done ? label : `${label}…`;\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const [width, setWidth] = useState<number>();\n\n  useLayoutEffect(() => {\n    if (measureRef.current) setWidth(measureRef.current.offsetWidth);\n  }, [text]);\n\n  return (\n    <div\n      aria-live=\"polite\"\n      className={cn(\n        \"inline-flex items-center gap-2 rounded-full border bg-card shadow-xs/5\",\n        size === \"sm\" ? \"h-7 ps-1.5 pe-3 text-xs\" : \"h-9 ps-2 pe-3.5 text-sm\",\n        className,\n      )}\n      role=\"status\"\n    >\n      <StatusSpinner done={done} size={size === \"sm\" ? 14 : 18} />\n      <motion.span\n        animate={{ width }}\n        className=\"relative inline-flex h-5 items-center overflow-hidden font-medium\"\n        initial={false}\n        transition={reduce ? { duration: 0 } : spring}\n      >\n        <span aria-hidden className=\"invisible absolute whitespace-nowrap\" ref={measureRef}>\n          {text}\n        </span>\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            animate={{ y: 0, opacity: 1, filter: \"blur(0px)\" }}\n            className=\"whitespace-nowrap\"\n            exit={reduce ? { opacity: 0 } : { y: -10, opacity: 0, filter: \"blur(2px)\" }}\n            initial={reduce ? { opacity: 0 } : { y: 10, opacity: 0, filter: \"blur(2px)\" }}\n            key={text}\n            transition={{ duration: 0.3, ease: easeOut }}\n          >\n            {done ? <span className=\"text-foreground\">{text}</span> : <ShimmerText>{text}</ShimmerText>}\n          </motion.span>\n        </AnimatePresence>\n      </motion.span>\n    </div>\n  );\n}\n\nfunction ShimmerText({ children }: { children: string }) {\n  const reduce = useMountedReducedMotion();\n  return (\n    <motion.span\n      animate={reduce ? undefined : { backgroundPosition: [\"100% 0%\", \"-100% 0%\"] }}\n      className=\"bg-clip-text text-transparent\"\n      style={{\n        backgroundImage:\n          \"linear-gradient(90deg, var(--color-muted-foreground) 35%, var(--color-foreground) 50%, var(--color-muted-foreground) 65%)\",\n        backgroundSize: \"200% 100%\",\n      }}\n      transition={{ duration: 1.6, ease: \"linear\", repeat: Number.POSITIVE_INFINITY }}\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nexport function StatusSpinner({ done = false, size = 18 }: { done?: boolean; size?: number }) {\n  const reduce = useMountedReducedMotion();\n\n  return (\n    <span aria-hidden className=\"relative inline-grid shrink-0 place-items-center\" style={{ width: size, height: size }}>\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        {done ? (\n          <motion.span\n            animate={{ scale: 1, opacity: 1 }}\n            className=\"absolute inset-0 grid place-items-center rounded-full bg-success text-white\"\n            exit={{ scale: 0.7, opacity: 0 }}\n            initial={reduce ? { opacity: 0 } : { scale: 0.6, opacity: 0 }}\n            key=\"done\"\n            transition={{ duration: 0.25, ease: easeOut }}\n          >\n            <svg\n              fill=\"none\"\n              height={size * 0.62}\n              stroke=\"currentColor\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth={3.2}\n              viewBox=\"0 0 24 24\"\n              width={size * 0.62}\n            >\n              <motion.path\n                animate={{ pathLength: 1 }}\n                d=\"M5 12.5 10 17.5 19 7\"\n                initial={{ pathLength: reduce ? 1 : 0 }}\n                transition={{ duration: 0.3, delay: 0.1, ease: \"easeOut\" }}\n              />\n            </svg>\n          </motion.span>\n        ) : (\n          <motion.svg\n            animate={reduce ? { opacity: 1 } : { opacity: 1, rotate: 360 }}\n            className=\"absolute inset-0\"\n            exit={{ opacity: 0, scale: 0.7, transition: { duration: 0.15 } }}\n            fill=\"none\"\n            initial={{ opacity: 0 }}\n            key=\"spin\"\n            transition={\n              reduce\n                ? { duration: 0.2 }\n                : { rotate: { duration: 0.8, ease: \"linear\", repeat: Number.POSITIVE_INFINITY }, opacity: { duration: 0.2 } }\n            }\n            viewBox=\"0 0 20 20\"\n          >\n            <circle className=\"stroke-border\" cx=\"10\" cy=\"10\" r=\"8\" strokeWidth=\"2.25\" />\n            <circle\n              className=\"stroke-foreground\"\n              cx=\"10\"\n              cy=\"10\"\n              pathLength={100}\n              r=\"8\"\n              strokeDasharray=\"28 100\"\n              strokeLinecap=\"round\"\n              strokeWidth=\"2.25\"\n            />\n          </motion.svg>\n        )}\n      </AnimatePresence>\n    </span>\n  );\n}\n"
    }
  ]
}