{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "alert",
  "type": "registry:component",
  "title": "Alert",
  "description": "A message set into the page that stays until it is dealt with, as a card in a tray of its tone.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "cssVars": {
    "light": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    },
    "dark": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    }
  },
  "files": [
    {
      "path": "components/panelui/alert.tsx",
      "type": "registry:component",
      "target": "@components/panelui/alert.tsx",
      "content": "'use client';\n\n/**\n * Alert — a message set into the page, which stays there until it has been\n * dealt with: a payment that failed, a trial running out, a form that needs\n * another look.\n *\n * ```tsx\n * <Alert tone=\"destructive\">\n *   <Alert.Icon />\n *   <Alert.Title>Your payment did not go through</Alert.Title>\n *   <Alert.Description>The card ending 4242 was declined.</Alert.Description>\n *   <Alert.Actions>\n *     <Button size=\"sm\">Update card</Button>\n *   </Alert.Actions>\n *   <Alert.Close />\n * </Alert>\n * ```\n *\n * ## A card in a tray of its tone\n *\n * By default the message is a card set into a tray tinted in its tone, and\n * the actions sit in the strip of tray left under the card. What the alert\n * says and what can be done about it are two surfaces, so a row of buttons\n * never crowds the sentence it answers. `plain` is a single tinted box, for\n * inside a form or a dialog that already draws an edge; `banner` is one line\n * across the top of a page or a panel.\n *\n * ## It folds away\n *\n * Closing it, with `Alert.Close` or `open`, folds it to no height instead of\n * removing it, so what is under it slides up rather than jumping. Nothing in\n * a closed alert can be focused.\n *\n * ## Interrupting is for trouble\n *\n * A `destructive` or `warning` alert is announced the moment it appears; the\n * others are read when the reader gets to them, because a note about a trial\n * should not cut across what a screen reader is in the middle of saying.\n */\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type * as ReactTypes from 'react';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\n/** The theme role an alert is drawn in. */\nexport type AlertTone = 'neutral' | 'info' | 'success' | 'warning' | 'destructive';\n\n/** `default` is the card in a tray, `plain` one tinted box, `banner` a strip across a page. */\nexport type AlertVariant = 'default' | 'plain' | 'banner';\n\n/** The fold every opening and closing here uses, the one the Prompt's bands use. */\nconst FOLD =\n  'grid transition-[grid-template-rows,opacity] duration-300 ease-[cubic-bezier(0.2,0,0,1)] motion-reduce:transition-none';\n\nconst alertVariants = tv({\n  slots: {\n    root: ['group/alert', FOLD],\n    clip: 'min-h-0 overflow-hidden',\n    /*\n     * The tray. The card fills it, so its one hairline is the alert's only\n     * edge; a rim of tint round the card read as a heavy frame. The tint\n     * shows in the strip under the card when there are actions, and at the\n     * card's lower corners, which round off over it.\n     */\n    tray: 'flex flex-col overflow-hidden rounded-[20px] border',\n    /*\n     * The parts are laid out by column, not by wrapper: the icon spans the\n     * rows at the start, the words run down the middle, and the close button\n     * holds the top of the end column. So the parts can be written in a flat\n     * list and still land where they belong.\n     */\n    body: [\n      'grid grid-cols-[auto_minmax(0,1fr)_auto] items-start text-sm',\n      // 19px is the tray's 20 less its hairline: one corner, two curves.\n      'group-data-[variant=default]/alert:rounded-[19px] group-data-[variant=default]/alert:bg-card group-data-[variant=default]/alert:px-3.5 group-data-[variant=default]/alert:py-3 group-data-[variant=default]/alert:text-card-foreground group-data-[variant=default]/alert:shadow-xs',\n      'group-data-[variant=plain]/alert:rounded-2xl group-data-[variant=plain]/alert:border group-data-[variant=plain]/alert:px-3.5 group-data-[variant=plain]/alert:py-3',\n      // One line: the parts sit in a row and wrap under each other when narrow.\n      'group-data-[variant=banner]/alert:flex group-data-[variant=banner]/alert:flex-wrap group-data-[variant=banner]/alert:items-center group-data-[variant=banner]/alert:gap-x-3 group-data-[variant=banner]/alert:gap-y-1 group-data-[variant=banner]/alert:border-b group-data-[variant=banner]/alert:px-4 group-data-[variant=banner]/alert:py-2.5',\n    ],\n    icon: [\n      'col-start-1 row-span-3 row-start-1 me-3 flex shrink-0 items-center justify-center text-[var(--pu-tone)] [&_svg]:shrink-0',\n      // In the card the glyph sits in a disc of its tone, the alert's one patch of colour.\n      'group-data-[variant=default]/alert:size-8 group-data-[variant=default]/alert:rounded-full group-data-[variant=default]/alert:bg-[color-mix(in_oklab,var(--pu-tone)_14%,transparent)] group-data-[variant=default]/alert:[&_svg:not([class*=size-])]:size-4',\n      // Elsewhere it is the glyph alone, centred on the title's line.\n      'group-data-[variant=plain]/alert:h-5 group-data-[variant=plain]/alert:[&_svg:not([class*=size-])]:size-4',\n      'group-data-[variant=banner]/alert:me-0 group-data-[variant=banner]/alert:[&_svg:not([class*=size-])]:size-4',\n    ],\n    title: [\n      'col-start-2 font-medium text-foreground',\n      // Beside the disc, the first line of the title sits on the disc's middle,\n      // and a title that wraps simply runs on below it.\n      'leading-5 group-data-[variant=default]/alert:pt-1.5',\n      // On its own it takes the line and wraps beside the icon; beside a description it keeps its width.\n      'group-data-[variant=banner]/alert:min-w-0 group-data-[variant=banner]/alert:flex-1 group-data-[variant=banner]/alert:basis-40 group-data-[variant=banner]/alert:group-has-[[data-slot=alert-description]]/alert:flex-none',\n    ],\n    description: [\n      'col-start-2 text-muted-foreground [&_a]:font-medium [&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-4',\n      // Alone beside the disc its first line sits on the disc's middle, as a title's would.\n      'group-data-[variant=default]/alert:pt-1.5 [[data-slot=alert-title]~&]:mt-0.5 group-data-[variant=default]/alert:[[data-slot=alert-title]~&]:pt-0',\n      'group-data-[variant=banner]/alert:min-w-0 group-data-[variant=banner]/alert:flex-1 group-data-[variant=banner]/alert:basis-48',\n    ],\n    actions: [\n      'flex flex-wrap items-center gap-2',\n      // In the tray's strip, at its end: what can be done follows what was said.\n      'group-data-[variant=default]/alert:justify-end group-data-[variant=default]/alert:p-2.5',\n      'group-data-[variant=plain]/alert:col-start-2 group-data-[variant=plain]/alert:mt-2.5',\n      'group-data-[variant=banner]/alert:ms-auto',\n    ],\n    close: [\n      'relative col-start-3 row-start-1 ms-2 inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground outline-none',\n      'transition-[background-color,color,scale] duration-150 after:absolute after:-inset-1.5 hover:bg-foreground/10 hover:text-foreground active:scale-95',\n      'focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none [&_svg]:size-3.5',\n      'group-data-[variant=default]/alert:-me-1 group-data-[variant=default]/alert:mt-0.5',\n      'group-data-[variant=plain]/alert:-me-1 group-data-[variant=plain]/alert:-mt-1',\n      'group-data-[variant=banner]/alert:-me-1.5',\n    ],\n    details: 'col-start-2 mt-2 flex flex-col',\n    detailsToggle: [\n      'inline-flex w-fit cursor-pointer items-center gap-1 rounded-md font-medium text-foreground outline-none',\n      'transition-colors hover:text-foreground/80 focus-visible:ring-2 focus-visible:ring-ring',\n    ],\n    detailsChevron: 'size-4 transition-transform duration-200 motion-reduce:transition-none',\n    detailsList: 'flex flex-col gap-1.5 pt-2',\n    item: [\n      'relative ps-4 text-muted-foreground',\n      // A dot in the tone, centred on the first line of the item.\n      \"before:absolute before:start-1 before:top-[0.5lh] before:size-1.5 before:-translate-y-1/2 before:rounded-full before:bg-[var(--pu-tone)] before:content-['']\",\n    ],\n  },\n  variants: {\n    tone: {\n      neutral: { root: '[--pu-tone:var(--muted-foreground)]' },\n      info: { root: '[--pu-tone:var(--info)]' },\n      success: { root: '[--pu-tone:var(--success)]' },\n      warning: { root: '[--pu-tone:var(--warning)]' },\n      destructive: { root: '[--pu-tone:var(--destructive)]' },\n    },\n    variant: {\n      default: {},\n      plain: {},\n      banner: {},\n    },\n    shown: {\n      true: { root: 'grid-rows-[1fr] opacity-100' },\n      false: { root: 'grid-rows-[0fr] opacity-0' },\n    },\n  },\n  compoundVariants: [\n    // The tray takes a little of the tone into the muted surface; neutral is the muted surface itself.\n    { variant: 'default', tone: 'neutral', className: { tray: 'bg-muted' } },\n    {\n      variant: 'default',\n      tone: ['info', 'success', 'warning', 'destructive'],\n      className: {\n        tray: 'border-[color-mix(in_oklab,var(--pu-tone)_30%,var(--border))] bg-[color-mix(in_oklab,var(--pu-tone)_10%,var(--muted))]',\n      },\n    },\n    { variant: 'plain', tone: 'neutral', className: { body: 'bg-muted/50' } },\n    {\n      variant: 'plain',\n      tone: ['info', 'success', 'warning', 'destructive'],\n      className: {\n        body: 'border-[color-mix(in_oklab,var(--pu-tone)_28%,var(--border))] bg-[color-mix(in_oklab,var(--pu-tone)_6%,var(--background))]',\n      },\n    },\n    { variant: 'banner', tone: 'neutral', className: { body: 'bg-muted' } },\n    {\n      variant: 'banner',\n      tone: ['info', 'success', 'warning', 'destructive'],\n      className: {\n        body: 'border-[color-mix(in_oklab,var(--pu-tone)_24%,var(--border))] bg-[color-mix(in_oklab,var(--pu-tone)_10%,var(--background))]',\n      },\n    },\n  ],\n  defaultVariants: { tone: 'neutral', variant: 'default', shown: true },\n});\n\ninterface AlertContextValue {\n  tone: AlertTone;\n  setOpen(open: boolean): void;\n}\n\nconst AlertContext = createContext<AlertContextValue | null>(null);\n\nfunction useAlert(part: string): AlertContextValue {\n  const alert = useContext(AlertContext);\n  if (!alert) throw new Error(`${part} must be used inside an <Alert>.`);\n  return alert;\n}\n\nexport interface AlertProps extends ComponentProps<'div'> {\n  /** The theme role it is drawn in, which also says how urgent it is. Default `neutral`. */\n  tone?: AlertTone;\n  /**\n   * `default` sets the message in a card inside a tray of its tone, with the\n   * actions in the tray under it. `plain` is one tinted box, for inside a form\n   * or a dialog. `banner` is one line across the top of a page or panel.\n   */\n  variant?: AlertVariant;\n  /** Whether it is shown, when the caller holds that. It folds away when it is not. */\n  open?: boolean;\n  /** Whether it starts shown, when the caller does not hold it. Default `true`. */\n  defaultOpen?: boolean;\n  /** Called when `Alert.Close` asks for it to close. */\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst isActions = (child: ReactNode) => isValidElement(child) && child.type === AlertActions;\n\nfunction AlertRoot({\n  tone = 'neutral',\n  variant = 'default',\n  open,\n  defaultOpen = true,\n  onOpenChange,\n  role,\n  className,\n  children,\n  ...props\n}: AlertProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const shown = open ?? internalOpen;\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [open, onOpenChange]\n  );\n  const clip = useRef<HTMLDivElement>(null);\n  // Nothing in a closed alert can be reached. Set on the element: how React\n  // writes a boolean `inert` differs between versions.\n  useEffect(() => {\n    clip.current?.toggleAttribute('inert', !shown);\n  }, [shown]);\n\n  const slots = alertVariants({ tone, variant, shown });\n  const context = useMemo(() => ({ tone, setOpen }), [tone, setOpen]);\n\n  // In the tray the actions leave the card for the strip under it.\n  const all = Children.toArray(children);\n  const actions = variant === 'default' ? all.filter(isActions) : [];\n  const rest = variant === 'default' ? all.filter((child) => !isActions(child)) : all;\n\n  const body = (\n    <div data-slot=\"alert-body\" className={slots.body()}>\n      {rest}\n    </div>\n  );\n\n  return (\n    <AlertContext.Provider value={context}>\n      <div\n        data-slot=\"alert\"\n        data-tone={tone}\n        data-variant={variant}\n        data-open={shown ? '' : undefined}\n        role={role ?? (tone === 'destructive' || tone === 'warning' ? 'alert' : 'status')}\n        className={slots.root({ className })}\n        {...props}\n      >\n        <div ref={clip} className={slots.clip()}>\n          {variant === 'default' ? (\n            <div data-slot=\"alert-tray\" className={slots.tray()}>\n              {body}\n              {actions}\n            </div>\n          ) : (\n            body\n          )}\n        </div>\n      </div>\n    </AlertContext.Provider>\n  );\n}\nAlertRoot.displayName = 'Alert';\n\n/* The glyph each tone draws when `Alert.Icon` is given none of its own. */\nconst GLYPHS: Record<AlertTone, ReactNode> = {\n  neutral: (\n    <>\n      <circle cx=\"12\" cy=\"12\" r=\"9\" />\n      <path d=\"M12 11v5M12 7.5h.01\" />\n    </>\n  ),\n  info: (\n    <>\n      <circle cx=\"12\" cy=\"12\" r=\"9\" />\n      <path d=\"M12 11v5M12 7.5h.01\" />\n    </>\n  ),\n  success: (\n    <>\n      <circle cx=\"12\" cy=\"12\" r=\"9\" />\n      <path d=\"m8.5 12.5 2.5 2.5 4.5-5\" />\n    </>\n  ),\n  warning: (\n    <>\n      <path d=\"M10.3 4.2 2.9 17a2 2 0 0 0 1.7 3h14.8a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0Z\" />\n      <path d=\"M12 9.5v4M12 17h.01\" />\n    </>\n  ),\n  destructive: (\n    <>\n      <circle cx=\"12\" cy=\"12\" r=\"9\" />\n      <path d=\"M12 7.5v5M12 16h.01\" />\n    </>\n  ),\n};\n\nexport interface AlertIconProps extends ComponentProps<'span'> {}\n\n/** The glyph at the start, in the tone's colour. Without children it draws the tone's own. */\nfunction AlertIcon({ className, children, render, ...props }: AlertIconProps & RenderProp<'span'>) {\n  const { tone } = useAlert('Alert.Icon');\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'alert-icon',\n        'aria-hidden': 'true',\n        className: alertVariants().icon({ className }),\n        children: (\n          <>\n            {children ?? (\n              <svg\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                {GLYPHS[tone]}\n              </svg>\n            )}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nAlertIcon.displayName = 'Alert.Icon';\n\nexport interface AlertTitleProps extends ComponentProps<'div'> {}\n\n/** What happened, in a few words. */\nfunction AlertTitle({ className, render, ...props }: AlertTitleProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'alert-title', className: alertVariants().title({ className }) }, props),\n  });\n}\nAlertTitle.displayName = 'Alert.Title';\n\nexport interface AlertDescriptionProps extends ComponentProps<'div'> {}\n\n/** The sentence under the title: why, and what follows from it. A link in it is underlined. */\nfunction AlertDescription({ className, render, ...props }: AlertDescriptionProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'alert-description', className: alertVariants().description({ className }) },\n      props\n    ),\n  });\n}\nAlertDescription.displayName = 'Alert.Description';\n\nexport interface AlertActionsProps extends ComponentProps<'div'> {}\n\n/**\n * The buttons. In the default variant they sit in the tray under the card, at\n * its end; in `plain` under the words; in `banner` at the end of the line.\n * Keep it a direct child of `Alert`, so the tray can find it.\n */\nfunction AlertActions({ className, render, ...props }: AlertActionsProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'alert-actions', className: alertVariants().actions({ className }) }, props),\n  });\n}\nAlertActions.displayName = 'Alert.Actions';\n\nexport interface AlertCloseProps extends ComponentProps<'button'> {}\n\n/** A round cross at the end that folds the alert away. */\nfunction AlertClose({\n  className,\n  onClick,\n  'aria-label': label = 'Dismiss',\n  render,\n  ...props\n}: AlertCloseProps & RenderProp<'button'>) {\n  const { setOpen } = useAlert('Alert.Close');\n  return useRender({\n    defaultTagName: 'button',\n    render,\n    props: mergeProps(\n      {\n        type: 'button',\n        'data-slot': 'alert-close',\n        'aria-label': label,\n        className: alertVariants().close({ className }),\n        onClick: (event: ReactTypes.MouseEvent<HTMLButtonElement>) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) setOpen(false);\n        },\n        children: (\n          <>\n            <svg\n              viewBox=\"0 0 24 24\"\n              aria-hidden=\"true\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2.4\"\n              strokeLinecap=\"round\"\n            >\n              <path d=\"M6 6l12 12M18 6L6 18\" />\n            </svg>\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nAlertClose.displayName = 'Alert.Close';\n\nexport interface AlertDetailsProps extends Omit<ComponentProps<'div'>, 'title'> {\n  /** The line that opens the list: \"Show the 3 fields\". */\n  label: ReactNode;\n  /** Whether the list is open, when the caller holds that. */\n  open?: boolean;\n  /** Whether the list starts open, when the caller does not hold it. Default `false`. */\n  defaultOpen?: boolean;\n  /** Called when the line is pressed to open or close the list. */\n  onOpenChange?: (open: boolean) => void;\n}\n\n/**\n * A line under the description that opens into a list of `Alert.Item`s, for\n * an alert that sums up several things: the fields a form still needs, the\n * files an upload skipped.\n */\nfunction AlertDetails({\n  label,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n  children,\n  render,\n  ...props\n}: AlertDetailsProps & RenderProp<'div'>) {\n  useAlert('Alert.Details');\n  const slots = alertVariants();\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const expanded = open ?? internalOpen;\n  const listId = useId();\n  const clip = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    clip.current?.toggleAttribute('inert', !expanded);\n  }, [expanded]);\n\n  const toggle = () => {\n    if (open === undefined) setInternalOpen(!expanded);\n    onOpenChange?.(!expanded);\n  };\n\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'alert-details',\n        'data-open': expanded ? '' : undefined,\n        className: slots.details({ className }),\n        children: (\n          <>\n            <button\n              type=\"button\"\n              aria-expanded={expanded}\n              aria-controls={listId}\n              onClick={toggle}\n              className={slots.detailsToggle()}\n            >\n              {label}\n              <svg\n                aria-hidden=\"true\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                className={slots.detailsChevron({ className: expanded ? 'rotate-180' : undefined })}\n              >\n                <path d=\"m6 9 6 6 6-6\" />\n              </svg>\n            </button>\n            <div className={[FOLD, expanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr] opacity-0'].join(' ')}>\n              <div ref={clip} className=\"min-h-0 overflow-hidden\">\n                <ul id={listId} className={slots.detailsList()}>\n                  {children}\n                </ul>\n              </div>\n            </div>\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nAlertDetails.displayName = 'Alert.Details';\n\nexport interface AlertItemProps extends ComponentProps<'li'> {}\n\n/** One entry in `Alert.Details`, marked with a dot in the alert's tone. */\nfunction AlertItem({ className, render, ...props }: AlertItemProps & RenderProp<'li'>) {\n  return useRender({\n    defaultTagName: 'li',\n    render,\n    props: mergeProps({ 'data-slot': 'alert-item', className: alertVariants().item({ className }) }, props),\n  });\n}\nAlertItem.displayName = 'Alert.Item';\n\nexport const Alert = Object.assign(AlertRoot, {\n  Icon: AlertIcon,\n  Title: AlertTitle,\n  Description: AlertDescription,\n  Actions: AlertActions,\n  Close: AlertClose,\n  Details: AlertDetails,\n  Item: AlertItem,\n});\n\nexport { AlertIcon, AlertTitle, AlertDescription, AlertActions, AlertClose, AlertDetails, AlertItem };\n"
    }
  ]
}
