{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"alert","type":"registry:component","title":"Animated Alert","description":"Composable feedback with smooth transitions.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/alert.tsx","type":"registry:component","target":"@components/motion/alert.tsx","content":"\"use client\";\n// beui.dev/components/motion/alert\n\nimport { AlertCircle, CheckCircle2, Info, TriangleAlert, X } from \"lucide-react\";\nimport { AnimatePresence, type HTMLMotionProps, motion, useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  type ComponentProps,\n  type RefObject,\n  useCallback,\n  useContext,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, EASE_OUT_CSS, SPRING_LAYOUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { PresenceGate } from \"@/lib/presence-gate\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AlertVariant = \"default\" | \"info\" | \"success\" | \"warning\" | \"destructive\";\n\nexport type AlertState = {\n  open: boolean;\n  variant: AlertVariant;\n  setOpen: (open: boolean) => void;\n  dismiss: () => void;\n};\n\nconst AlertContext = createContext<(AlertState & { reduce: boolean }) | null>(null);\n\nfunction useAlertContext(component: string) {\n  const context = useContext(AlertContext);\n  if (!context) throw new Error(`${component} must be used within <Alert>`);\n  return context;\n}\n\n/** Shared visibility and variant for custom alert children. */\nexport function useAlert(): AlertState {\n  return useAlertContext(\"useAlert\");\n}\n\nconst VARIANT_CLASS: Record<AlertVariant, string> = {\n  default: \"bg-background text-foreground\",\n  info: \"bg-primary/5 text-primary\",\n  success: \"bg-emerald-500/5 text-emerald-700 dark:text-emerald-400\",\n  warning: \"bg-amber-500/5 text-amber-800 dark:text-amber-300\",\n  destructive: \"bg-destructive/5 text-destructive\",\n};\n\nconst VARIANT_ICON = {\n  default: Info,\n  info: Info,\n  success: CheckCircle2,\n  warning: TriangleAlert,\n  destructive: AlertCircle,\n};\n\nexport interface AlertProps extends Omit<HTMLMotionProps<\"div\">, \"initial\" | \"animate\" | \"exit\" | \"layout\" | \"transition\" | \"role\"> {\n  variant?: AlertVariant;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Focus this element when closing hides the currently focused alert control. */\n  returnFocusRef?: RefObject<HTMLElement | null>;\n  /** Layout classes for the inner row, separate from the animated surface. */\n  contentClassName?: string;\n  /** Defaults to alert for warning/destructive, and status for other variants. */\n  role?: \"alert\" | \"status\";\n}\n\nexport function Alert({\n  variant = \"default\",\n  open: controlledOpen,\n  defaultOpen = true,\n  onOpenChange,\n  returnFocusRef,\n  contentClassName,\n  className,\n  children,\n  role = variant === \"warning\" || variant === \"destructive\" ? \"alert\" : \"status\",\n  style,\n  ref,\n  ...props\n}: AlertProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const open = controlledOpen ?? internalOpen;\n  const reduce = useReducedMotion() ?? false;\n  const surfaceRef = useRef<HTMLDivElement>(null);\n  const stateRef = useRef({ open, controlled: controlledOpen !== undefined, onOpenChange });\n\n  useLayoutEffect(() => {\n    stateRef.current = { open, controlled: controlledOpen !== undefined, onOpenChange };\n  }, [controlledOpen, onOpenChange, open]);\n\n  const setOpen = useCallback((next: boolean) => {\n    const state = stateRef.current;\n    if (next === state.open) return;\n    if (!state.controlled) {\n      stateRef.current = { ...state, open: next };\n      setInternalOpen(next);\n    }\n    state.onOpenChange?.(next);\n  }, []);\n  const dismiss = useCallback(() => setOpen(false), [setOpen]);\n  const context = useMemo(() => ({ open, variant, reduce, setOpen, dismiss }), [open, variant, reduce, setOpen, dismiss]);\n\n  const setRef = useCallback((node: HTMLDivElement | null) => {\n    surfaceRef.current = node;\n    const cleanup = typeof ref === \"function\" ? ref(node) : undefined;\n    if (ref && typeof ref !== \"function\") ref.current = node;\n    if (!node) return;\n    return () => {\n      surfaceRef.current = null;\n      if (typeof cleanup === \"function\") cleanup();\n      else if (typeof ref === \"function\") ref(null);\n      else if (ref) ref.current = null;\n    };\n  }, [ref]);\n\n  useLayoutEffect(() => {\n    if (!open && surfaceRef.current?.contains(document.activeElement)) {\n      returnFocusRef?.current?.focus({ preventScroll: true });\n    }\n  }, [open, returnFocusRef]);\n\n  return (\n    <AlertContext.Provider value={context}>\n      <AnimatePresence initial={false}>\n        {open && (\n          <PresenceGate key=\"alert\">\n            {({ gate, isPresent }) => (\n              <motion.div\n                {...props}\n                {...gate}\n                ref={setRef}\n                role={role}\n                aria-hidden={!isPresent || props[\"aria-hidden\"]}\n                data-slot=\"alert\"\n                data-variant={variant}\n                data-state={isPresent ? \"open\" : \"closed\"}\n                initial={{ opacity: 0, transform: reduce ? \"none\" : \"translateY(-6px)\" }}\n                animate={{ opacity: 1, transform: \"none\" }}\n                exit={{ opacity: 0, transform: reduce ? \"none\" : \"translateY(-6px)\", transition: { duration: 0.12, ease: EASE_OUT } }}\n                layout={!reduce}\n                transition={{ opacity: { duration: 0.18, ease: EASE_OUT }, transform: { duration: 0.2, ease: EASE_OUT }, layout: SPRING_LAYOUT }}\n                style={{ ...style, ...gate.style, originY: 0, transitionTimingFunction: EASE_OUT_CSS }}\n                className={cn(\"relative w-full rounded-xl p-4 text-sm transition-colors duration-200\", VARIANT_CLASS[variant], className)}\n              >\n                <motion.div\n                  layout={reduce ? false : \"position\"}\n                  transition={{ layout: SPRING_LAYOUT }}\n                  style={{ originY: 0 }}\n                  className={cn(\"flex min-w-0 items-start gap-3\", contentClassName)}\n                >\n                  {children}\n                </motion.div>\n              </motion.div>\n            )}\n          </PresenceGate>\n        )}\n      </AnimatePresence>\n    </AlertContext.Provider>\n  );\n}\n\nexport interface AlertIconProps extends ComponentProps<\"span\"> {}\n\nexport function AlertIcon({ className, children, ...props }: AlertIconProps) {\n  const { variant, reduce } = useAlertContext(\"AlertIcon\");\n  const Icon = VARIANT_ICON[variant];\n  return (\n    <span {...props} aria-hidden=\"true\" data-slot=\"alert-icon\" className={cn(\"mt-0.5 inline-flex size-4 shrink-0 items-center justify-center\", className)}>\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          key={variant}\n          initial={{ opacity: 0, transform: reduce ? \"translateY(0px) scale(1)\" : \"translateY(4px) scale(0.9)\" }}\n          animate={{ opacity: 1, transform: \"translateY(0px) scale(1)\" }}\n          exit={{ opacity: 0, transform: reduce ? \"translateY(0px) scale(1)\" : \"translateY(-4px) scale(0.9)\", transition: { duration: 0.1, ease: EASE_OUT } }}\n          transition={{ duration: 0.18, ease: EASE_OUT }}\n          className=\"inline-flex [&>svg]:size-4\"\n        >\n          {children ?? <Icon />}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nexport interface AlertContentProps extends ComponentProps<\"div\"> {}\n\nexport function AlertContent({ className, ...props }: AlertContentProps) {\n  return <div {...props} data-slot=\"alert-content\" className={cn(\"min-w-0 flex-1 space-y-1\", className)} />;\n}\n\nexport interface AlertTitleProps extends ComponentProps<\"div\"> {}\n\nexport function AlertTitle({ className, ...props }: AlertTitleProps) {\n  return <div {...props} data-slot=\"alert-title\" className={cn(\"font-medium leading-5 text-pretty\", className)} />;\n}\n\nexport interface AlertDescriptionProps extends ComponentProps<\"div\"> {}\n\nexport function AlertDescription({ className, ...props }: AlertDescriptionProps) {\n  return <div {...props} data-slot=\"alert-description\" className={cn(\"text-pretty leading-relaxed text-muted-foreground [&_a]:underline [&_a]:underline-offset-4\", className)} />;\n}\n\nexport interface AlertActionProps extends ComponentProps<\"div\"> {}\n\n/** Compose buttons or links here; this part never creates a nested button. */\nexport function AlertAction({ className, ...props }: AlertActionProps) {\n  return <div {...props} data-slot=\"alert-action\" className={cn(\"flex flex-wrap items-center gap-2 pt-2\", className)} />;\n}\n\nexport interface AlertCloseProps extends Omit<HTMLMotionProps<\"button\">, \"layout\"> {}\n\nexport function AlertClose({ className, children, onClick, ...props }: AlertCloseProps) {\n  const { dismiss, reduce } = useAlertContext(\"AlertClose\");\n  return (\n    <motion.button\n      type=\"button\"\n      aria-label=\"Dismiss alert\"\n      {...props}\n      tabIndex={props.tabIndex ?? 0}\n      data-slot=\"alert-close\"\n      layout={reduce ? false : \"position\"}\n      transition={SPRING_PRESS}\n      whileTap={reduce ? undefined : { scale: 0.94 }}\n      className={cn(\"-m-1.5 inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground outline-none transition-colors hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\", className)}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) dismiss();\n      }}\n    >\n      {children ?? <X aria-hidden=\"true\" className=\"size-4\" />}\n    </motion.button>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/presence-gate.tsx","type":"registry:lib","target":"@lib/presence-gate.tsx","content":"\"use client\";\n\nimport { useIsPresent } from \"motion/react\";\nimport type { ReactNode } from \"react\";\n\nexport interface PresenceGateRenderProps {\n  /**\n   * False from the render that starts the exit animation onward. An overlay\n   * kept in the tree by `AnimatePresence` is still the topmost thing on the\n   * page, so anything it decides from `open` alone stays true for the whole\n   * exit — this is the boolean that already knows the overlay is leaving.\n   */\n  isPresent: boolean;\n  /**\n   * Spread onto every layer that takes pointer events while the overlay is\n   * open. Interaction releases in the same commit that starts the exit while\n   * the visual exit keeps playing: pointer events stop landing, and `inert`\n   * drops the subtree from focus order, from tab order and from the\n   * accessibility tree — an exiting dialog is not a dialog you can still type\n   * into. A layer that never takes pointer events (a wrapper that only centres\n   * the panel) takes `inert={!isPresent}` alone, so its own\n   * `pointer-events-none` is not overwritten.\n   */\n  gate: {\n    inert: boolean;\n    style: { pointerEvents: \"auto\" | \"none\" };\n  };\n}\n\nexport interface PresenceGateProps {\n  children: (props: PresenceGateRenderProps) => ReactNode;\n}\n\n/**\n * Reads the presence of the subtree it renders and hands it down.\n *\n * `useIsPresent` only answers inside the `AnimatePresence` subtree, and the\n * components that own an overlay render the `AnimatePresence` themselves, so\n * the boolean has to be read one component further down: this is that\n * component, and the render prop is how it reaches the layers.\n */\nexport function PresenceGate({ children }: PresenceGateProps) {\n  const isPresent = useIsPresent();\n\n  return children({\n    isPresent,\n    gate: {\n      inert: !isPresent,\n      style: { pointerEvents: isPresent ? \"auto\" : \"none\" },\n    },\n  });\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}