{
  "name": "inline-alert",
  "type": "registry:component",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @kokonutui\n * @description: Inline Alert\n * @version: 1.0.0\n * @date: 2026-11-08\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport {\n  ChevronDown,\n  CircleAlert,\n  CircleCheck,\n  LoaderCircle,\n  Rocket,\n  X,\n} from \"lucide-react\";\nimport { AnimatePresence, MotionConfig, motion } from \"motion/react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface AlertCheck {\n  name: string;\n  /** Why it failed, shown under the name. */\n  detail?: string;\n  ok: boolean;\n}\n\nexport interface InlineAlertProps {\n  checks?: AlertCheck[];\n  /** Where the checks ran, shown in the message. */\n  target?: string;\n  /** ms each failed check takes to pass again. */\n  retryMs?: number;\n  onDeploy?: () => void;\n  className?: string;\n}\n\nconst DEMO_CHECKS: AlertCheck[] = [\n  { name: \"Build\", ok: true },\n  { name: \"Unit tests\", detail: \"3 failing in auth.test.ts\", ok: false },\n  { name: \"Lint\", ok: true },\n  { name: \"Type check\", detail: \"2 errors in billing.ts\", ok: false },\n];\n\ntype Phase = \"failed\" | \"retrying\" | \"passed\" | \"deploying\" | \"deployed\";\n\nconst ICON_TONE: Record<Phase, string> = {\n  failed: \"bg-rose-500\",\n  retrying: \"bg-sky-500\",\n  passed: \"bg-emerald-500\",\n  deploying: \"bg-emerald-500\",\n  deployed: \"bg-emerald-500\",\n};\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst DEPLOY_MS = 1400;\n\nexport default function InlineAlert({\n  checks: initialChecks = DEMO_CHECKS,\n  target = \"main · 4f2a91c\",\n  retryMs = 900,\n  onDeploy,\n  className,\n}: InlineAlertProps) {\n  const allPass = initialChecks.every((c) => c.ok);\n  const [checks, setChecks] = useState(initialChecks);\n  const [running, setRunning] = useState<string | null>(null);\n  const [phase, setPhase] = useState<Phase>(allPass ? \"passed\" : \"failed\");\n  const [open, setOpen] = useState(true);\n  const [visible, setVisible] = useState(true);\n  const timer = useRef<number>(undefined);\n\n  useEffect(() => () => window.clearTimeout(timer.current), []);\n\n  const failed = checks.filter((c) => !c.ok);\n  const passedShare = (checks.length - failed.length) / checks.length;\n\n  // Re-run the failed checks one after another; each one passes this time.\n  const retry = () => {\n    setPhase(\"retrying\");\n    setOpen(true);\n    const queue = failed.map((c) => c.name);\n    const step = (i: number) => {\n      if (i >= queue.length) {\n        setRunning(null);\n        setPhase(\"passed\");\n        return;\n      }\n      setRunning(queue[i]);\n      timer.current = window.setTimeout(() => {\n        setChecks((all) =>\n          all.map((c) =>\n            c.name === queue[i] ? { ...c, ok: true, detail: undefined } : c\n          )\n        );\n        step(i + 1);\n      }, retryMs);\n    };\n    step(0);\n  };\n\n  const deploy = () => {\n    setPhase(\"deploying\");\n    timer.current = window.setTimeout(() => {\n      setPhase(\"deployed\");\n      setOpen(false);\n      onDeploy?.();\n    }, DEPLOY_MS);\n  };\n\n  const reset = () => {\n    window.clearTimeout(timer.current);\n    setChecks(initialChecks);\n    setRunning(null);\n    setPhase(allPass ? \"passed\" : \"failed\");\n    setOpen(true);\n    setVisible(true);\n  };\n\n  const copy: Record<Phase, [string, string]> = {\n    failed: [\n      \"Deploy blocked\",\n      `${failed.length} of ${checks.length} checks failed on ${target}`,\n    ],\n    retrying: [\"Running checks again\", `${failed.length} left on ${target}`],\n    passed: [\"Ready to deploy\", `All ${checks.length} checks passed`],\n    deploying: [\"Deploying\", \"Uploading to production\"],\n    deployed: [\"Deployed\", \"Live on production · just now\"],\n  };\n  const [title, message] = copy[phase];\n  // Green wipes in as checks pass; it stays put once they all have.\n  let green = 0;\n  if (phase === \"retrying\") {\n    green = passedShare;\n  } else if (phase !== \"failed\") {\n    green = 1;\n  }\n  const showChecks = phase !== \"deployed\";\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div className={cn(\"w-full max-w-md\", className)}>\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {visible ? (\n            <motion.div\n              animate={{ opacity: 1, y: 0, scale: 1 }}\n              className=\"relative overflow-hidden rounded-2xl text-zinc-900 dark:text-zinc-50\"\n              exit={{\n                opacity: 0,\n                y: -4,\n                scale: 0.98,\n                transition: { duration: 0.15 },\n              }}\n              initial={{ opacity: 0, y: 6, scale: 0.98 }}\n              key=\"alert\"\n              role={phase === \"failed\" ? \"alert\" : \"status\"}\n              transition={{ duration: 0.25, ease: EASE_OUT }}\n            >\n              <div\n                aria-hidden=\"true\"\n                className=\"absolute inset-0 rounded-2xl border border-rose-200 bg-rose-50 dark:border-rose-900/60 dark:bg-rose-950/40\"\n              />\n              <motion.div\n                animate={{ clipPath: `inset(0% ${(1 - green) * 100}% 0% 0%)` }}\n                aria-hidden=\"true\"\n                className=\"absolute inset-0 rounded-2xl border border-emerald-200 bg-emerald-50 dark:border-emerald-900/60 dark:bg-emerald-950/40\"\n                initial={false}\n                transition={{ duration: 0.6, ease: EASE_OUT }}\n              />\n\n              <div className=\"relative flex gap-3 p-4\">\n                <span\n                  className={cn(\n                    \"relative flex size-7 shrink-0 items-center justify-center rounded-full text-white transition-colors duration-300\",\n                    ICON_TONE[phase]\n                  )}\n                >\n                  <AnimatePresence initial={false} mode=\"popLayout\">\n                    <motion.span\n                      animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n                      exit={{ opacity: 0, scale: 0.8, filter: \"blur(2px)\" }}\n                      initial={{ opacity: 0, scale: 0.8, filter: \"blur(2px)\" }}\n                      key={phase === \"deploying\" ? \"passed\" : phase}\n                      transition={{ duration: 0.2, ease: EASE_OUT }}\n                    >\n                      <PhaseIcon phase={phase} />\n                    </motion.span>\n                  </AnimatePresence>\n                </span>\n\n                <div className=\"min-w-0 flex-1 pt-0.5\">\n                  <div className=\"flex items-start justify-between gap-3\">\n                    <div aria-live=\"polite\" className=\"relative min-w-0\">\n                      <AnimatePresence initial={false} mode=\"popLayout\">\n                        <motion.div\n                          animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                          exit={{ opacity: 0, y: -4, filter: \"blur(2px)\" }}\n                          initial={{ opacity: 0, y: 4, filter: \"blur(2px)\" }}\n                          key={`${title}${message}`}\n                          transition={{ duration: 0.2, ease: EASE_OUT }}\n                        >\n                          <p className=\"font-semibold text-[14.5px] leading-5\">\n                            {title}\n                          </p>\n                          <p className=\"mt-0.5 text-[13px] text-zinc-600 tabular-nums dark:text-zinc-400\">\n                            {message}\n                          </p>\n                        </motion.div>\n                      </AnimatePresence>\n                    </div>\n                    <button\n                      aria-label=\"Dismiss\"\n                      className=\"-mt-1 -mr-1 flex size-7 shrink-0 items-center justify-center rounded-full text-zinc-500 outline-hidden transition-colors duration-150 hover:bg-black/5 hover:text-zinc-900 focus-visible:ring-2 focus-visible:ring-zinc-400/50 active:bg-black/10 dark:text-zinc-400 dark:active:bg-white/20 dark:hover:bg-white/10 dark:hover:text-zinc-100\"\n                      onClick={() => setVisible(false)}\n                      type=\"button\"\n                    >\n                      <X aria-hidden=\"true\" className=\"size-4\" />\n                    </button>\n                  </div>\n\n                  <AnimatePresence initial={false}>\n                    {showChecks && open && (\n                      <motion.ul\n                        animate={{\n                          height: \"auto\",\n                          opacity: 1,\n                          transition: {\n                            height: { duration: 0.3, ease: EASE_OUT },\n                            opacity: { duration: 0.2, delay: 0.08 },\n                          },\n                        }}\n                        className=\"overflow-hidden\"\n                        exit={{\n                          height: 0,\n                          opacity: 0,\n                          transition: {\n                            height: { duration: 0.25, ease: EASE_OUT },\n                            opacity: { duration: 0.12 },\n                          },\n                        }}\n                        initial={{ height: 0, opacity: 0 }}\n                      >\n                        {checks.map((check) => (\n                          <CheckRow\n                            check={check}\n                            key={check.name}\n                            running={running === check.name}\n                          />\n                        ))}\n                      </motion.ul>\n                    )}\n                  </AnimatePresence>\n\n                  <AnimatePresence initial={false}>\n                    {phase !== \"deploying\" && phase !== \"deployed\" && (\n                      <motion.div\n                        animate={{ height: \"auto\", opacity: 1 }}\n                        className=\"overflow-hidden\"\n                        exit={{ height: 0, opacity: 0 }}\n                        initial={{ height: 0, opacity: 0 }}\n                        transition={{ duration: 0.2, ease: EASE_OUT }}\n                      >\n                        <div className=\"flex items-center gap-2 pt-3\">\n                          {phase === \"failed\" && (\n                            <AlertButton onClick={retry} primary>\n                              Retry {failed.length} failed\n                            </AlertButton>\n                          )}\n                          {phase === \"passed\" && (\n                            <AlertButton onClick={deploy} primary>\n                              <Rocket aria-hidden=\"true\" className=\"size-3.5\" />\n                              Deploy\n                            </AlertButton>\n                          )}\n                          <AlertButton\n                            ariaExpanded={open}\n                            onClick={() => setOpen(!open)}\n                          >\n                            {open ? \"Hide checks\" : \"Show checks\"}\n                            <ChevronDown\n                              aria-hidden=\"true\"\n                              className={cn(\n                                \"size-3.5 transition-transform duration-200\",\n                                open && \"rotate-180\"\n                              )}\n                            />\n                          </AlertButton>\n                        </div>\n                      </motion.div>\n                    )}\n                  </AnimatePresence>\n                </div>\n              </div>\n\n              {/* Deploy progress runs along the bottom edge. */}\n              <AnimatePresence>\n                {phase === \"deploying\" && (\n                  <motion.span\n                    animate={{ scaleX: 1 }}\n                    aria-hidden=\"true\"\n                    className=\"absolute inset-x-0 bottom-0 h-[3px] origin-left bg-emerald-500\"\n                    exit={{ opacity: 0, transition: { duration: 0.3 } }}\n                    initial={{ scaleX: 0 }}\n                    transition={{\n                      duration: DEPLOY_MS / 1000,\n                      ease: [0.33, 0.6, 0.4, 1],\n                    }}\n                  />\n                )}\n              </AnimatePresence>\n            </motion.div>\n          ) : (\n            <motion.button\n              animate={{ opacity: 1 }}\n              className=\"h-10 w-full rounded-2xl border border-zinc-300 border-dashed text-[13px] text-zinc-500 outline-hidden transition-colors duration-150 hover:text-zinc-800 focus-visible:ring-2 focus-visible:ring-zinc-400/50 dark:border-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-100\"\n              initial={{ opacity: 0 }}\n              key=\"restore\"\n              onClick={reset}\n              transition={{ duration: 0.2, delay: 0.1 }}\n              type=\"button\"\n            >\n              Bring the alert back\n            </motion.button>\n          )}\n        </AnimatePresence>\n      </div>\n    </MotionConfig>\n  );\n}\n\nfunction PhaseIcon({ phase }: { phase: Phase }) {\n  if (phase === \"retrying\") {\n    return <LoaderCircle aria-hidden=\"true\" className=\"size-4 animate-spin\" />;\n  }\n  if (phase === \"deployed\") {\n    return <Rocket aria-hidden=\"true\" className=\"size-4\" />;\n  }\n  if (phase === \"passed\" || phase === \"deploying\") {\n    return <CircleCheck aria-hidden=\"true\" className=\"size-4\" />;\n  }\n  return <CircleAlert aria-hidden=\"true\" className=\"size-4\" />;\n}\n\nfunction CheckRow({ check, running }: { check: AlertCheck; running: boolean }) {\n  let status = \"Failed\";\n  if (running) {\n    status = \"Running\";\n  } else if (check.ok) {\n    status = \"Passed\";\n  }\n  return (\n    <li className=\"flex items-start gap-2.5 pt-2.5 first:pt-3\">\n      <span className=\"relative mt-0.5 flex size-4 shrink-0 items-center justify-center\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n            className=\"flex\"\n            exit={{ opacity: 0, scale: 0.8, filter: \"blur(2px)\" }}\n            initial={{ opacity: 0, scale: 0.8, filter: \"blur(2px)\" }}\n            key={status}\n            transition={{ duration: 0.18, ease: EASE_OUT }}\n          >\n            <CheckGlyph status={status} />\n          </motion.span>\n        </AnimatePresence>\n      </span>\n      <span className=\"min-w-0 flex-1 text-[13px]\">\n        <span className=\"flex items-center justify-between gap-2\">\n          <span className=\"font-medium\">{check.name}</span>\n          <span className=\"relative text-[12px] text-zinc-500 dark:text-zinc-400\">\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.span\n                animate={{ opacity: 1, y: 0 }}\n                className=\"block\"\n                exit={{ opacity: 0, y: -4 }}\n                initial={{ opacity: 0, y: 4 }}\n                key={status}\n                transition={{ duration: 0.18, ease: EASE_OUT }}\n              >\n                {status}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n        </span>\n        <AnimatePresence initial={false}>\n          {check.detail && (\n            <motion.span\n              animate={{ height: \"auto\", opacity: 1 }}\n              className=\"block overflow-hidden\"\n              exit={{\n                height: 0,\n                opacity: 0,\n                transition: {\n                  height: { duration: 0.25, ease: EASE_OUT },\n                  opacity: { duration: 0.12 },\n                },\n              }}\n              initial={{ height: 0, opacity: 0 }}\n            >\n              <span\n                className={cn(\n                  \"block font-mono text-[11.5px] text-zinc-600 transition-opacity duration-200 dark:text-zinc-400\",\n                  running && \"opacity-50\"\n                )}\n              >\n                {check.detail}\n              </span>\n            </motion.span>\n          )}\n        </AnimatePresence>\n      </span>\n    </li>\n  );\n}\n\nfunction CheckGlyph({ status }: { status: string }) {\n  if (status === \"Running\") {\n    return (\n      <LoaderCircle\n        aria-hidden=\"true\"\n        className=\"size-4 animate-spin text-sky-500\"\n      />\n    );\n  }\n  if (status === \"Passed\") {\n    return (\n      <svg\n        aria-hidden=\"true\"\n        className=\"size-4 text-emerald-500\"\n        fill=\"none\"\n        viewBox=\"0 0 16 16\"\n      >\n        <circle cx={8} cy={8} fill=\"currentColor\" opacity={0.15} r={8} />\n        <motion.path\n          animate={{ pathLength: 1 }}\n          d=\"M5 8.2l2 2 4-4.2\"\n          initial={{ pathLength: 0 }}\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth={1.8}\n          transition={{ duration: 0.25, delay: 0.05, ease: EASE_OUT }}\n        />\n      </svg>\n    );\n  }\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-4 text-rose-500\"\n      fill=\"none\"\n      viewBox=\"0 0 16 16\"\n    >\n      <circle cx={8} cy={8} fill=\"currentColor\" opacity={0.15} r={8} />\n      <path\n        d=\"M5.8 5.8l4.4 4.4M10.2 5.8l-4.4 4.4\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeWidth={1.8}\n      />\n    </svg>\n  );\n}\n\nfunction AlertButton({\n  onClick,\n  primary,\n  ariaExpanded,\n  children,\n}: {\n  onClick: () => void;\n  primary?: boolean;\n  ariaExpanded?: boolean;\n  children: ReactNode;\n}) {\n  return (\n    <button\n      aria-expanded={ariaExpanded}\n      className={cn(\n        \"flex h-8 items-center gap-1.5 rounded-full px-3 font-medium text-[12.5px] outline-hidden transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-zinc-400/50\",\n        primary\n          ? \"bg-zinc-900 text-white hover:bg-zinc-800 active:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:active:bg-zinc-300 dark:hover:bg-zinc-200\"\n          : \"bg-black/5 hover:bg-black/10 active:bg-black/15 dark:bg-white/10 dark:active:bg-white/20 dark:hover:bg-white/15\"\n      )}\n      onClick={onClick}\n      type=\"button\"\n    >\n      {children}\n    </button>\n  );\n}\n",
      "path": "/components/kokonutui/inline-alert.tsx",
      "target": "components/kokonutui/inline-alert.tsx"
    }
  ]
}