{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"alert","type":"registry:component","title":"Alert","description":"Inline alert banner in info, success, warning and destructive tones that keep contrast in light and dark, with icons, a title, description and optional actions, polite status or assertive alert roles by severity, and a dismiss that fades the banner and collapses its height so the content below slides up, plus an AlertStack whose gaps collapse with it.","author":"kara <ztzyrant@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// karaui.kamisama-v0.my.id/components/motion/alert\n\nimport {\n  CircleCheck,\n  Info,\n  type LucideIcon,\n  OctagonAlert,\n  TriangleAlert,\n  X,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentPropsWithoutRef,\n  type ReactNode,\n  createContext,\n  useContext,\n  useState,\n} from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AlertVariant = \"info\" | \"success\" | \"warning\" | \"destructive\";\n\n// Each tone is mixed toward --foreground: darker in light mode, lighter in\n// dark mode, so icon and title tint keep contrast on either background while\n// still coming from the theme tokens.\nconst VARIANT_CLASS: Record<AlertVariant, { root: string; icon: string }> = {\n  info: {\n    root: \"border-[color-mix(in_oklab,var(--chart-1)_30%,transparent)] bg-[color-mix(in_oklab,var(--chart-1)_8%,var(--background))]\",\n    icon: \"text-[color-mix(in_oklab,var(--chart-1)_75%,var(--foreground))]\",\n  },\n  success: {\n    root: \"border-[color-mix(in_oklab,var(--success)_32%,transparent)] bg-[color-mix(in_oklab,var(--success)_9%,var(--background))]\",\n    icon: \"text-[color-mix(in_oklab,var(--success)_62%,var(--foreground))]\",\n  },\n  warning: {\n    root: \"border-[color-mix(in_oklab,var(--warning)_40%,transparent)] bg-[color-mix(in_oklab,var(--warning)_10%,var(--background))]\",\n    icon: \"text-[color-mix(in_oklab,var(--warning)_55%,var(--foreground))]\",\n  },\n  destructive: {\n    root: \"border-[color-mix(in_oklab,var(--destructive)_35%,transparent)] bg-[color-mix(in_oklab,var(--destructive)_8%,var(--background))]\",\n    icon: \"text-[color-mix(in_oklab,var(--destructive)_75%,var(--foreground))]\",\n  },\n};\n\nconst VARIANT_ICON: Record<AlertVariant, LucideIcon> = {\n  info: Info,\n  success: CircleCheck,\n  warning: TriangleAlert,\n  destructive: OctagonAlert,\n};\n\nconst StackContext = createContext(false);\n\nexport interface AlertProps\n  extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\" | \"role\"> {\n  variant?: AlertVariant;\n  title?: ReactNode;\n  description?: ReactNode;\n  /** Replace the variant icon; `null` hides it. */\n  icon?: ReactNode | null;\n  /** Buttons rendered under the text (e.g. library `Button size=\"sm\"`). */\n  actions?: ReactNode;\n  /** Show a dismiss (X) button. */\n  dismissible?: boolean;\n  dismissLabel?: string;\n  /** Controlled visibility. Leave undefined to let the Alert hide itself on dismiss. */\n  open?: boolean;\n  /** Called when the dismiss button is pressed. */\n  onDismiss?: () => void;\n}\n\n/**\n * Inline banner for info / success / warning / destructive messages.\n * Info and success announce politely (`role=\"status\"`); warning and\n * destructive interrupt (`role=\"alert\"`). Dismissing fades the banner and\n * collapses its height so content below closes the gap.\n */\nexport function Alert({\n  variant = \"info\",\n  title,\n  description,\n  icon,\n  actions,\n  dismissible = false,\n  dismissLabel = \"Dismiss\",\n  open: openProp,\n  onDismiss,\n  className,\n  children,\n  ...rest\n}: AlertProps) {\n  const reduce = useReducedMotion() ?? false;\n  const inStack = useContext(StackContext);\n  const [internalOpen, setInternalOpen] = useState(true);\n  const open = openProp ?? internalOpen;\n  const Icon = VARIANT_ICON[variant];\n  const tone = VARIANT_CLASS[variant];\n  const urgent = variant === \"warning\" || variant === \"destructive\";\n\n  const dismiss = () => {\n    if (openProp === undefined) setInternalOpen(false);\n    onDismiss?.();\n  };\n\n  // Height is animated on purpose: the collapse is the component's job, so the\n  // content below slides up instead of jumping. Under reduced motion the\n  // banner only fades, then the height snaps once it is invisible.\n  const collapsed = reduce\n    ? {\n        opacity: 0,\n        height: 0,\n        transition: { opacity: { duration: 0.15 }, height: { delay: 0.15, duration: 0 } },\n      }\n    : {\n        opacity: 0,\n        height: 0,\n        transition: {\n          opacity: { duration: 0.14, ease: EASE_OUT },\n          height: { duration: 0.24, ease: EASE_OUT },\n        },\n      };\n\n  return (\n    <AnimatePresence initial={false}>\n      {open ? (\n        <motion.div\n          key=\"alert\"\n          initial={{ opacity: 0, height: 0 }}\n          animate={{\n            opacity: 1,\n            height: \"auto\",\n            transition: reduce\n              ? { opacity: { duration: 0.2 }, height: { duration: 0 } }\n              : {\n                  height: { duration: 0.26, ease: EASE_OUT },\n                  opacity: { duration: 0.22, delay: 0.06, ease: EASE_OUT },\n                },\n          }}\n          exit={collapsed}\n          className={cn(\"overflow-hidden\", inStack && \"pb-3\")}\n        >\n          <div\n            role={urgent ? \"alert\" : \"status\"}\n            data-variant={variant}\n            className={cn(\n              \"relative flex gap-3 rounded-2xl border p-4 text-sm text-foreground\",\n              tone.root,\n              className,\n            )}\n            {...rest}\n          >\n            {icon === null ? null : (\n              <span\n                aria-hidden=\"true\"\n                className={cn(\"mt-px shrink-0 [&_svg]:size-[18px]\", tone.icon)}\n              >\n                {icon ?? <Icon strokeWidth={2.2} />}\n              </span>\n            )}\n            <div className={cn(\"flex min-w-0 flex-1 flex-col gap-1\", dismissible && \"pr-7\")}>\n              {title ? (\n                <p className=\"font-semibold leading-5 tracking-tight\">\n                  {title}\n                </p>\n              ) : null}\n              {description ? (\n                <div className=\"leading-relaxed text-foreground/75\">{description}</div>\n              ) : null}\n              {children}\n              {actions ? <div className=\"mt-2 flex flex-wrap gap-2\">{actions}</div> : null}\n            </div>\n            {dismissible ? (\n              <button\n                type=\"button\"\n                aria-label={dismissLabel}\n                onClick={dismiss}\n                className=\"absolute top-3 right-3 inline-flex size-7 items-center justify-center rounded-full text-foreground/60 transition-colors hover:bg-foreground/[0.07] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n              >\n                <X className=\"size-4\" aria-hidden=\"true\" />\n              </button>\n            ) : null}\n          </div>\n        </motion.div>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n\nexport interface AlertStackProps extends ComponentPropsWithoutRef<\"div\"> {}\n\n/**\n * Vertical list of Alerts whose spacing collapses together with a dismissed\n * alert (the gap lives inside each alert's collapsing wrapper, not in `gap`).\n */\nexport function AlertStack({ className, children, ...rest }: AlertStackProps) {\n  return (\n    <StackContext.Provider value>\n      <div className={cn(\"-mb-3 flex flex-col\", className)} {...rest}>\n        {children}\n      </div>\n    </StackContext.Provider>\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/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"}]}