{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"alert","type":"registry:component","title":"Alert","description":"A message that sits in the page: its tint, line and icon ease when the tone changes, and dismissing folds it away so the content below slides up.","author":"Hairline UI","dependencies":[],"registryDependencies":["https://hairlineui.com/r/ui-interaction-motion.json?as=dependency"],"files":[{"path":"registry/alert.tsx","type":"registry:component","content":"// Hairline UI · alert · MIT License\n// Copyright (c) 2026 Hairline UI · https://hairlineui.com/license#components\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { EASE_OUT, INTERACTION_FOCUS, INTERACTION_PRESS, interactionIcon, useInteractionReducedMotion } from \"./ui-interaction-motion\";\n\n/* ─────────────────────────────────────────────────────────\n * ALERT: a message that sits in the page\n *\n *   tone     info, success, warning or danger: the tint, line and\n *            icon ease across when the tone changes (a sync that\n *            goes from \"Syncing\" to \"Synced\"), the icon swapping\n *            through a blur\n *   dismiss  the ✕ folds the alert away: it fades and lifts while\n *            its height closes, so the content below slides up\n *            instead of jumping\n *   action   an optional button on the right (or under it on\n *            narrow cards)\n * ───────────────────────────────────────────────────────── */\n\nexport type AlertTone = \"info\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface AlertProps {\n  tone?: AlertTone;\n  title: React.ReactNode;\n  children?: React.ReactNode;\n  /** A button or link at the end. */\n  action?: React.ReactNode;\n  /** Show a ✕; called after the alert has folded away. */\n  onDismiss?: () => void;\n  /** Hide it (folds away) without unmounting. */\n  hidden?: boolean;\n  /** Replace the tone's icon. */\n  icon?: React.ReactNode;\n  className?: string;\n}\n\nconst TONE: Record<AlertTone, string> = {\n  info: \"bg-card shadow-[inset_0_0_0_1px_var(--border)] text-foreground\",\n  success: \"bg-emerald-500/[0.06] shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--color-emerald-500)_30%,transparent)] text-foreground\",\n  warning: \"bg-amber-500/[0.07] shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--color-amber-500)_35%,transparent)] text-foreground\",\n  danger: \"bg-red-500/[0.06] shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--color-red-500)_30%,transparent)] text-foreground\",\n};\nconst ICON_TONE: Record<AlertTone, string> = {\n  info: \"text-muted-foreground\",\n  success: \"text-emerald-600 dark:text-emerald-400\",\n  warning: \"text-amber-600 dark:text-amber-400\",\n  danger: \"text-red-600 dark:text-red-400\",\n};\n\nconst ICONS: Record<AlertTone, React.ReactNode> = {\n  info: (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-4\">\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n      <path d=\"M8 7.25v3.5\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n      <circle cx=\"8\" cy=\"5\" r=\"0.9\" fill=\"currentColor\" />\n    </svg>\n  ),\n  success: (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-4\">\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n      <path d=\"M5.4 8.2 7.2 10l3.4-3.8\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  ),\n  warning: (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-4\">\n      <path d=\"M8 2.2 14.3 13H1.7L8 2.2Z\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinejoin=\"round\" />\n      <path d=\"M8 6.3v3.2\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n      <circle cx=\"8\" cy=\"11.2\" r=\"0.85\" fill=\"currentColor\" />\n    </svg>\n  ),\n  danger: (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-4\">\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n      <path d=\"M8 4.75v3.75\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n      <circle cx=\"8\" cy=\"11\" r=\"0.9\" fill=\"currentColor\" />\n    </svg>\n  ),\n};\n\nexport function Alert({ tone = \"info\", title, children, action, onDismiss, hidden: hiddenProp, icon, className = \"\" }: AlertProps) {\n  const reduced = useInteractionReducedMotion();\n  const [dismissed, setDismissed] = React.useState(false);\n  const hidden = hiddenProp || dismissed;\n\n  React.useEffect(() => {\n    if (!dismissed) return;\n    const timer = window.setTimeout(() => onDismiss?.(), reduced ? 0 : 380);\n    return () => window.clearTimeout(timer);\n  }, [dismissed, onDismiss, reduced]);\n\n  return (\n    <div\n      className=\"grid\"\n      aria-hidden={hidden || undefined}\n      inert={hidden || undefined}\n      style={{ gridTemplateRows: hidden ? \"0fr\" : \"1fr\", transition: reduced ? \"none\" : `grid-template-rows 380ms ${EASE_OUT} ${hidden ? 80 : 0}ms` }}\n    >\n      <div className=\"min-h-0 overflow-hidden [clip-path:inset(-2px)]\">\n        <div\n          role={tone === \"danger\" || tone === \"warning\" ? \"alert\" : \"status\"}\n          className={`@container flex items-start gap-3 rounded-[14px] p-3.5 transition-[background-color,box-shadow] duration-300 ${TONE[tone]} ${className}`}\n          style={{\n            opacity: hidden ? 0 : 1,\n            translate: hidden && !reduced ? \"0 -6px\" : \"0 0\",\n            scale: hidden && !reduced ? \"0.98\" : \"1\",\n            transition: reduced ? \"none\" : `opacity 220ms ${EASE_OUT}, translate 320ms ${EASE_OUT}, scale 320ms ${EASE_OUT}, background-color 300ms, box-shadow 300ms`,\n          }}\n        >\n          <span className={`relative mt-px size-4 shrink-0 transition-colors duration-300 ${ICON_TONE[tone]}`} aria-hidden=\"true\">\n            {icon ? (\n              icon\n            ) : (\n              (Object.keys(ICONS) as AlertTone[]).map((t) => (\n                <span key={t} className=\"absolute inset-0\" style={interactionIcon(t === tone, reduced)}>\n                  {ICONS[t]}\n                </span>\n              ))\n            )}\n          </span>\n          <div className=\"flex min-w-0 flex-1 flex-col gap-2.5 @md:flex-row @md:items-start @md:justify-between\">\n            <div className=\"flex min-w-0 flex-col gap-0.5\">\n              <p className=\"text-[13.5px] font-medium leading-5\">{title}</p>\n              {children && <div className=\"text-[13px] leading-relaxed text-muted-foreground\">{children}</div>}\n            </div>\n            {action && <div className=\"flex shrink-0 items-center gap-2\">{action}</div>}\n          </div>\n          {onDismiss && (\n            <button\n              type=\"button\"\n              aria-label=\"Dismiss\"\n              onClick={() => setDismissed(true)}\n              className={`-mr-1 -mt-1 flex size-7 shrink-0 items-center justify-center rounded-full text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground ${INTERACTION_PRESS} ${INTERACTION_FOCUS}`}\n            >\n              <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-3.5\">\n                <path d=\"M4 4l8 8M12 4l-8 8\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n              </svg>\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default Alert;\n"}],"css":{},"docs":"Docs and live preview: https://hairlineui.com/ui/alert. MIT licensed: https://hairlineui.com/license#components","meta":{"license":"MIT"}}