{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "alert",
  "title": "Alert",
  "description": "An animated alert for React and shadcn, for context, success, warning and error messages.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-auto-height.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/alert/alert.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { AutoHeight } from \"../_primitives/auto-height/auto-height\";\nimport { exitFast, reducedFade, swap, swapIn, swapOut, swapShown, useSafeReducedMotion } from \"../lib/motion\";\nimport { pressIcon } from \"../lib/press\";\n\nexport type AlertTone = \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\";\n\nexport type AlertProps = {\n  /** neutral for notes, info for context, success after it worked, warning before it might not, danger when it did not. Default \"info\". */\n  tone?: AlertTone;\n  /** One line, in sentence case. */\n  title: ReactNode;\n  /** Detail under the title: what happened and what to do next. */\n  children?: ReactNode;\n  /** Replace the tone's icon (16px), or `null` for none. */\n  icon?: ReactNode;\n  /** Buttons under the description, e.g. a small secondary \"Retry\". */\n  actions?: ReactNode;\n  /** Shows a close button that calls this. Pair with `open` to animate the alert away. */\n  onDismiss?: () => void;\n  /** False collapses the alert and fades it out; true springs it open. Default true. */\n  open?: boolean;\n  /**\n   * \"status\" (default) is announced politely when it appears; \"alert\" interrupts, for errors\n   * the person must act on now. \"none\" for alerts present on page load.\n   */\n  role?: \"status\" | \"alert\" | \"none\";\n  className?: string;\n  /**\n   * Classes for the inner wrapper that collapses with the alert. Put the gap above a\n   * dismissible alert here (`pt-3`) instead of on a flex parent, so it closes with the alert.\n   */\n  innerClassName?: string;\n};\n\nconst TONE: Record<AlertTone, { box: string; icon: string }> = {\n  neutral: { box: \"border-line-subtle bg-surface-sunken\", icon: \"text-text-secondary\" },\n  info: { box: \"border-info/20 bg-info-subtle\", icon: \"text-info-text\" },\n  success: { box: \"border-success/20 bg-success-subtle\", icon: \"text-success-text\" },\n  warning: { box: \"border-warning/25 bg-warning-subtle\", icon: \"text-warning-text\" },\n  danger: { box: \"border-danger/20 bg-danger-subtle\", icon: \"text-danger-text\" },\n};\n\nconst path = { fill: \"none\", stroke: \"currentColor\", strokeWidth: 1.5, strokeLinecap: \"round\", strokeLinejoin: \"round\" } as const;\n\nconst ICONS: Record<AlertTone, ReactNode> = {\n  neutral: (\n    <svg viewBox=\"0 0 16 16\" className=\"size-4\">\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" {...path} />\n      <path d=\"M8 7.25v3.5M8 5.25v.01\" {...path} />\n    </svg>\n  ),\n  info: (\n    <svg viewBox=\"0 0 16 16\" className=\"size-4\">\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" {...path} />\n      <path d=\"M8 7.25v3.5M8 5.25v.01\" {...path} />\n    </svg>\n  ),\n  success: (\n    <svg viewBox=\"0 0 16 16\" className=\"size-4\">\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" {...path} />\n      <path d=\"M5.5 8.25 7.25 10l3.25-3.75\" {...path} />\n    </svg>\n  ),\n  warning: (\n    <svg viewBox=\"0 0 16 16\" className=\"size-4\">\n      <path d=\"M7.13 2.5a1 1 0 0 1 1.74 0l5.2 9.25a1 1 0 0 1-.87 1.5H2.8a1 1 0 0 1-.87-1.5Z\" {...path} />\n      <path d=\"M8 6.5v2.75M8 11.25v.01\" {...path} />\n    </svg>\n  ),\n  danger: (\n    <svg viewBox=\"0 0 16 16\" className=\"size-4\">\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" {...path} />\n      <path d=\"M8 4.75V8.5M8 10.75v.01\" {...path} />\n    </svg>\n  ),\n};\n\nconst FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Where focus goes when the alert is dismissed: the next focusable element among its siblings\n * (past its single-child wrappers), else the previous one. The alert itself turns inert as it\n * collapses, so it is only the last resort.\n */\nfunction focusTarget(root: HTMLElement) {\n  let own: HTMLElement = root;\n  while (own.parentElement && own.parentElement.childElementCount === 1) own = own.parentElement;\n  const candidates = [...(own.parentElement?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])].filter(\n    (el) => !root.contains(el) && el.getClientRects().length > 0,\n  );\n  const after = candidates.find((el) => root.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING);\n  return after ?? candidates.at(-1) ?? null;\n}\n\n/**\n * An inline callout for context, success, warnings and errors, tinted by tone. It springs\n * open and closed on its own height, so the page below glides instead of jumping; when the\n * tone changes the tint cross-fades and the icon swaps with a blur.\n */\nexport function Alert({ tone = \"info\", title, children, icon, actions, onDismiss, open = true, role = \"status\", className = \"\", innerClassName = \"\" }: AlertProps) {\n  const reduced = useSafeReducedMotion();\n  const glyph = icon === undefined ? ICONS[tone] : icon;\n  const box = useRef<HTMLDivElement>(null);\n\n  // The Dismiss button is about to collapse away: hand focus on before it drops to <body>.\n  const dismiss = () => {\n    const root = box.current;\n    if (root?.contains(document.activeElement)) (focusTarget(root) ?? root).focus();\n    onDismiss?.();\n  };\n\n  return (\n    <AutoHeight open={open} className={className} innerClassName={innerClassName}>\n      <div\n        ref={box}\n        tabIndex={-1}\n        role={role === \"none\" ? undefined : role}\n        className={`grid grid-cols-[auto_minmax(0,1fr)_auto] gap-x-2.5 rounded-control border px-3 py-2.5 outline-none transition-colors duration-150 ease-standard ${TONE[tone].box}`}\n      >\n        {/* Icon centred on the title's 20px first line (rule 58). */}\n        <span aria-hidden className={`relative flex h-5 w-4 items-center transition-colors duration-150 ease-standard ${glyph ? \"\" : \"hidden\"} ${TONE[tone].icon}`}>\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            <motion.span\n              key={tone}\n              className=\"flex\"\n              initial={reduced ? { opacity: 0 } : swapIn}\n              animate={reduced ? { opacity: 1 } : swapShown}\n              exit={reduced ? { opacity: 0, transition: reducedFade } : { ...swapOut, transition: exitFast }}\n              transition={reduced ? reducedFade : swap}\n            >\n              {glyph}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n        <div className={`flex min-w-0 flex-col gap-0.5 ${glyph ? \"\" : \"col-span-2\"}`}>\n          <p className=\"text-ui font-medium text-text text-pretty\">{title}</p>\n          {children != null && <div className=\"text-ui-sm text-text-secondary text-pretty\">{children}</div>}\n          {actions != null && <div className=\"mt-2 flex flex-wrap items-center gap-2\">{actions}</div>}\n        </div>\n        {onDismiss ? (\n          <button\n            type=\"button\"\n            aria-label=\"Dismiss\"\n            onClick={dismiss}\n            className={`-me-1 -mt-0.5 flex size-6 cursor-pointer items-center justify-center rounded-control-xs text-text-muted hover-fine:bg-surface-hover hover-fine:text-text ${pressIcon}`}\n          >\n            <svg viewBox=\"0 0 16 16\" aria-hidden className=\"size-3.5\">\n              <path d=\"M4.5 4.5l7 7M11.5 4.5l-7 7\" {...path} />\n            </svg>\n          </button>\n        ) : (\n          <span />\n        )}\n      </div>\n    </AutoHeight>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/alert/alert.tsx"
    }
  ],
  "meta": {
    "details": "An inline callout for context, success, warnings and errors. It springs open and closed on its own height, and when the tone changes the tint cross-fades and the icon swaps.",
    "industries": [
      "ai",
      "devtools",
      "finance",
      "crypto",
      "saas",
      "legal",
      "ecommerce",
      "real-estate",
      "health",
      "hr",
      "education",
      "travel",
      "media"
    ],
    "releaseAt": "2026-10-21"
  },
  "docs": "Import Alert from components/eased/alert/alert. Props: tone (neutral, info, success, warning, danger), title, children, icon, actions, onDismiss, open, role, innerClassName. The Eased tokens were merged into your global CSS; nothing to import.",
  "type": "registry:ui"
}