{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"alert","type":"registry:ui","title":"Alert","description":"Inline alert that stays in the page flow in six tones, as a floating card or full-width banner; it unrolls with a spring, each tone icon moves once, and dismissing folds it away.","author":"GetUrUI <hello@geturui.dev>","dependencies":["motion","lucide-react"],"registryDependencies":["@geturui/button"],"files":[{"path":"registry/geturui/ui/alert.tsx","type":"registry:ui","target":"components/ui/alert.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, type AnimationPlaybackControls } from \"motion/react\";\nimport { ArrowRight, Bell, Info, OctagonAlert, Sparkles, TriangleAlert, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { buttonClasses } from \"@/components/ui/button-classes\";\n\nexport type AlertTone = \"neutral\" | \"brand\" | \"info\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface AlertAction {\n  label: string;\n  onClick?: () => void;\n  /** Renders a link instead of a button. */\n  href?: string;\n  variant?: \"primary\" | \"secondary\" | \"link\";\n}\n\nexport interface AlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n  tone?: AlertTone;\n  /** \"floating\" = card in the page flow; \"banner\" = full-width bar, e.g. above a page or app shell. */\n  layout?: \"floating\" | \"banner\";\n  size?: \"sm\" | \"md\" | \"lg\";\n  title?: React.ReactNode;\n  /** Supporting text. `children` works too. */\n  description?: React.ReactNode;\n  /** Replace the tone icon, or pass false to hide it. */\n  icon?: React.ReactNode | false;\n  actions?: AlertAction[];\n  /** Show a close button. */\n  dismissible?: boolean;\n  dismissLabel?: string;\n  /** Visible state, controlled. */\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Auto-dismiss after this many ms. A hairline counts down and pauses on hover or focus. */\n  duration?: number;\n}\n\nconst tones: Record<AlertTone, { tint: string; ink: string; tile: string; border: string }> = {\n  neutral: { tint: \"\", ink: \"text-ink-muted\", tile: \"bg-surface-hover text-ink\", border: \"border-border\" },\n  brand: { tint: \"bg-volt-soft\", ink: \"text-volt-ink\", tile: \"bg-volt text-on-volt\", border: \"border-volt-ink/30\" },\n  info: { tint: \"bg-info-soft\", ink: \"text-info\", tile: \"bg-info-soft text-info\", border: \"border-info/30\" },\n  success: { tint: \"bg-success-soft\", ink: \"text-success\", tile: \"bg-success-soft text-success\", border: \"border-success/30\" },\n  warning: { tint: \"bg-warning-soft\", ink: \"text-warning\", tile: \"bg-warning-soft text-warning\", border: \"border-warning/35\" },\n  danger: { tint: \"bg-danger-soft\", ink: \"text-danger\", tile: \"bg-danger-soft text-danger\", border: \"border-danger/30\" },\n};\n\nconst sizing = {\n  sm: { pad: \"p-3\", gap: \"gap-2.5\", title: \"text-[13px]\", body: \"text-[13px]\", tile: \"size-7 rounded-xs\", glyph: \"size-3.5\", btn: \"sm\" as const, banner: \"px-3 py-2\" },\n  md: { pad: \"p-4\", gap: \"gap-3\", title: \"text-sm\", body: \"text-sm\", tile: \"size-9 rounded-sm\", glyph: \"size-[18px]\", btn: \"sm\" as const, banner: \"px-4 py-2.5\" },\n  lg: { pad: \"p-5\", gap: \"gap-4\", title: \"text-base\", body: \"text-[15px]\", tile: \"size-11 rounded-sm\", glyph: \"size-5\", btn: \"md\" as const, banner: \"px-5 py-3.5\" },\n};\n\n/** Success tick that draws itself once. */\nfunction DrawnCheck({ className, reduce }: { className: string; reduce: boolean }) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className} aria-hidden>\n      <circle cx=\"12\" cy=\"12\" r=\"9.5\" />\n      <motion.path\n        d=\"m7.75 12.25 2.75 2.75 5.75-6\"\n        initial={{ pathLength: reduce ? 1 : 0 }}\n        animate={{ pathLength: 1 }}\n        transition={{ type: \"spring\", stiffness: 160, damping: 20, delay: 0.12 }}\n      />\n    </svg>\n  );\n}\n\n/** Each tone gets one small, one-time motion: draw, nudge, shake or pulse. */\nfunction ToneIcon({ tone, className, reduce }: { tone: AlertTone; className: string; reduce: boolean }) {\n  if (tone === \"success\") return <DrawnCheck className={className} reduce={reduce} />;\n  const Glyph = { neutral: Bell, brand: Sparkles, info: Info, warning: TriangleAlert, danger: OctagonAlert }[tone];\n  const motionFor: Record<string, { animate: Record<string, number[]>; transition: Record<string, number | string> }> = {\n    neutral: { animate: { rotate: [0, -14, 10, -6, 0] }, transition: { duration: 0.7, delay: 0.1 } },\n    brand: { animate: { scale: [1, 1.18, 1], rotate: [0, 8, 0] }, transition: { duration: 0.6, delay: 0.1 } },\n    info: { animate: { scale: [1, 1.15, 1] }, transition: { duration: 0.5, delay: 0.1 } },\n    warning: { animate: { y: [0, -2.5, 0, -1.5, 0] }, transition: { duration: 0.6, delay: 0.1 } },\n    danger: { animate: { x: [0, -2.5, 2.5, -1.5, 1.5, 0] }, transition: { duration: 0.45, delay: 0.1 } },\n  };\n  const m = motionFor[tone];\n  return (\n    <motion.span aria-hidden className=\"inline-flex\" animate={reduce ? undefined : m.animate} transition={m.transition}>\n      <Glyph className={className} strokeWidth={2} />\n    </motion.span>\n  );\n}\n\n/** Inline alert that stays in the page flow (unlike Toast): six tones, floating card or full-width banner, actions and an animated dismiss. */\nexport function Alert({\n  tone = \"neutral\",\n  layout = \"floating\",\n  size = \"md\",\n  title,\n  description,\n  icon,\n  actions,\n  dismissible,\n  dismissLabel = \"Dismiss\",\n  open,\n  defaultOpen = true,\n  onOpenChange,\n  duration,\n  className,\n  children,\n  ...props\n}: AlertProps) {\n  const reduce = !!useReducedMotion();\n  const [inner, setInner] = React.useState(defaultOpen);\n  const isOpen = open ?? inner;\n  const t = tones[tone];\n  const s = sizing[size];\n  const banner = layout === \"banner\";\n  const body = description ?? children;\n\n  const setOpen = React.useCallback(\n    (v: boolean) => {\n      if (open === undefined) setInner(v);\n      onOpenChange?.(v);\n    },\n    [open, onOpenChange],\n  );\n\n  // Countdown hairline: a motion value we can pause while the pointer or focus is inside.\n  const progress = useMotionValue(1);\n  const controls = React.useRef<AnimationPlaybackControls | null>(null);\n  React.useEffect(() => {\n    if (!isOpen || !duration) return;\n    progress.set(1);\n    controls.current = animate(progress, 0, { duration: duration / 1000, ease: \"linear\", onComplete: () => setOpen(false) });\n    return () => controls.current?.stop();\n  }, [isOpen, duration, progress, setOpen]);\n  const pause = () => controls.current?.pause();\n  const resume = () => controls.current?.play();\n\n  const primaryVariant = tone === \"brand\" ? \"primary\" : \"inverse\";\n\n  const actionRow = actions?.length ? (\n    <div className={cn(\"flex flex-wrap items-center gap-x-2 gap-y-1.5\", banner ? \"\" : \"mt-3\")}>\n      {actions.map((a) => {\n        const cls =\n          a.variant === \"link\"\n            ? cn(\n                \"group/link inline-flex items-center gap-1 rounded-xs font-semibold underline-offset-4 hover:underline outline-none focus-visible:shadow-[var(--focus-ring)]\",\n                s.body,\n                t.ink === \"text-ink-muted\" ? \"text-ink\" : t.ink,\n              )\n            : buttonClasses({ variant: a.variant === \"secondary\" ? \"ghost\" : primaryVariant, size: s.btn, className: a.variant === \"secondary\" ? \"text-ink\" : undefined });\n        const inner = (\n          <>\n            {a.label}\n            {a.variant === \"link\" && <ArrowRight aria-hidden className=\"size-3.5 transition-transform duration-300 ease-spring group-hover/link:translate-x-0.5\" />}\n          </>\n        );\n        return a.href ? (\n          <a key={a.label} href={a.href} onClick={a.onClick} className={cls}>\n            {inner}\n          </a>\n        ) : (\n          <button key={a.label} type=\"button\" onClick={a.onClick} className={cls}>\n            {inner}\n          </button>\n        );\n      })}\n    </div>\n  ) : null;\n\n  const glyph =\n    icon === false ? null : (\n      <span className={cn(\"flex shrink-0 items-center justify-center\", banner ? cn(\"size-5\", t.ink === \"text-ink-muted\" ? \"text-ink\" : t.ink) : cn(s.tile, t.tile))}>\n        {icon ?? <ToneIcon tone={tone} className={banner ? \"size-[18px]\" : s.glyph} reduce={reduce} />}\n      </span>\n    );\n\n  const close = dismissible ? (\n    <button\n      type=\"button\"\n      onClick={() => setOpen(false)}\n      aria-label={dismissLabel}\n      className=\"-m-1 inline-flex size-8 shrink-0 items-center justify-center rounded-full text-ink-subtle transition-colors hover:bg-surface-hover hover:text-ink outline-none focus-visible:shadow-[var(--focus-ring)]\"\n    >\n      <X aria-hidden className=\"size-4\" />\n    </button>\n  ) : null;\n\n  return (\n    <AnimatePresence initial={false}>\n      {isOpen && (\n        <motion.div\n          key=\"alert\"\n          initial={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}\n          animate={reduce ? { opacity: 1 } : { height: \"auto\", opacity: 1 }}\n          exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0, filter: \"blur(6px)\" }}\n          transition={{ type: \"spring\", stiffness: 320, damping: 34, opacity: { duration: 0.2 } }}\n          className={cn(\"overflow-hidden\", !banner && \"-m-2 p-2\")}\n        >\n          <div\n            role={tone === \"danger\" || tone === \"warning\" ? \"alert\" : \"status\"}\n            onPointerEnter={pause}\n            onPointerLeave={resume}\n            onFocus={pause}\n            onBlur={resume}\n            className={cn(\n              \"relative isolate overflow-hidden text-ink\",\n              banner ? cn(\"w-full border-y\", t.border, s.banner) : cn(\"rounded-md border bg-surface-raised shadow-md\", t.border, s.pad),\n              banner && tone === \"neutral\" && \"bg-surface\",\n              className,\n            )}\n            {...props}\n          >\n            {t.tint && <span aria-hidden className={cn(\"absolute inset-0 -z-10\", t.tint)} />}\n\n            {banner ? (\n              <div className={cn(\"mx-auto flex flex-wrap items-center gap-x-3 gap-y-2 sm:flex-nowrap\", s.gap)}>\n                {glyph}\n                <p className={cn(\"min-w-0 flex-1\", s.body)}>\n                  {title && <span className=\"font-semibold\">{title}</span>}\n                  {title && body ? <span className=\"text-ink-muted\"> {body}</span> : <span className=\"text-ink-muted\">{body}</span>}\n                </p>\n                {actionRow && <div className=\"order-last w-full pl-8 sm:order-none sm:w-auto sm:pl-0\">{actionRow}</div>}\n                {close}\n              </div>\n            ) : (\n              <div className={cn(\"flex items-start\", s.gap)}>\n                {glyph}\n                <div className={cn(\"min-w-0 flex-1\", icon !== false && size !== \"sm\" && \"pt-0.5\", icon !== false && size === \"sm\" && \"pt-1\")}>\n                  {title && <p className={cn(\"font-semibold leading-snug\", s.title)}>{title}</p>}\n                  {body && <div className={cn(\"leading-relaxed text-ink-muted\", s.body, title && \"mt-0.5\")}>{body}</div>}\n                  {actionRow}\n                </div>\n                {close}\n              </div>\n            )}\n\n            {duration ? (\n              <motion.span\n                aria-hidden\n                style={{ scaleX: progress }}\n                className={cn(\"absolute inset-x-0 bottom-0 h-0.5 origin-left opacity-70\", tone === \"neutral\" ? \"bg-ink-subtle\" : \"bg-current\", t.ink)}\n              />\n            ) : null}\n          </div>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n"},{"path":"LICENSE-geturui.md","type":"registry:file","target":"LICENSE-geturui.md","content":"MIT License\n\nCopyright (c) 2026 geturui.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"}],"categories":["components","alert","banner","callout","notice","status","feedback","message"],"meta":{"tier":"free","license":"MIT","docs":"https://geturui.com/docs/alert"}}