{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "type": "registry:ui",
  "title": "Toast",
  "description": "@interlace/ui — toast primitive (shadcn-compatible).",
  "dependencies": [
    "@base-ui/react",
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "theme"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/toast.tsx",
      "target": "components/ui/toast.tsx",
      "type": "registry:ui",
      "content": "'use client';\n\n/**\n * @interlace/ui — Toast\n *\n * Transient, portal-mounted notification. Wraps `@base-ui/react/toast`\n * (Provider, Portal, Viewport, Root, Title, Description, Close) and overlays\n * the Interlace tone system on top — four semantic tones (`info`, `success`,\n * `warning`, `danger`) that each render a 4px left accent strip, so the toast\n * reads without colour alone (paired with a consumer-supplied icon per\n * `ERROR_PHILOSOPHY` + `DESIGN_PRINCIPLES` #9).\n *\n * ## Anatomy\n *\n *   ToastProvider (Base UI manager — owns the timer/limit context)\n *     └─ ToastViewport (data-slot, portalled container)\n *         └─ Toast (root · data-slot · data-min-viewport · tone strip)\n *             ├─ ToastTitle      (data-slot)\n *             ├─ ToastDescription (data-slot, muted)\n *             └─ ToastClose      (data-slot, top-right X)\n *\n * ## MIN_VIEWPORT — 320\n *\n * Toasts must remain readable on the smallest baseline viewport (iPhone SE,\n * 320 CSS-px). The viewport positions itself away from the safe-area edges\n * via padding from `--spacing-sm`, the surface itself ships at full-width\n * within the viewport container, and the tone strip never displaces text —\n * so the visual weight is identical at 320px and at 1280px.\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R4   | Extends Base UI part + variants  | `React.ComponentProps<typeof BaseToast.Root>` + cva tone    |\n * | R6   | data-slot on every named part    | data-slot=\"toast\" / \"-title\" / \"-description\" / \"-close\" / \"-viewport\" / \"-provider\" |\n * | R7   | className merged + ...rest       | `cn(toastVariants({ tone }), className)` + `{...props}` on every part |\n * | R8   | No `isXxx`; enum for >2 states   | `tone` enum: info / success / warning / danger              |\n * | R13  | Build with the ecosystem         | `@base-ui/react/toast` owns lifecycle, ARIA, focus, portal  |\n * | R14  | Declares min viewport            | `data-min-viewport={String(MIN_VIEWPORT)}` + exported const |\n * | R17  | API parity with Base UI + shadcn | Re-exports mirror Base UI part names with Interlace prefix  |\n * | R18  | Tailwind only                    | Zero inline `style`; cva + static animation classes only    |\n * | R19  | Tokens only                      | `bg-card` / `text-card-foreground` / `border-border` / `rounded-md` / `shadow-lg` / padding from `--spacing-md` |\n * | R20  | AA contrast                      | card + card-foreground are AA-cleared in light + dark; tone strip is a non-text channel |\n * | R23  | CLS=0                            | Animations are transform/opacity only; viewport portals out of the layout flow |\n * | R26  | A11y from headless primitive     | role / aria-live / focus-trap inherited from Base UI Root   |\n *\n * Motion contract (DESIGN_PRINCIPLES #7, MOTION_PHILOSOPHY): slide-from-right\n * + fade-in over 180ms ease-out on open; reverse on close. `useReducedMotion`\n * gates the slide so reduced-motion users get a fade-only transition (still\n * ≤200ms, no transform).\n */\n\nimport * as React from 'react';\nimport { Toast as BaseToast } from '@base-ui/react/toast';\nimport { useRender } from '@base-ui/react/use-render';\nimport { XIcon } from 'lucide-react';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\nimport { useReducedMotion } from '@/hooks/use-reduced-motion';\n\n// Signals that Toast sub-parts are inside a static (screenshot) render\n// path — no BaseToast.Root context available. Sub-parts fall back to\n// plain semantic HTML so they render without Base UI's Root context.\nconst ToastStaticCtx = React.createContext(false);\n\n/**\n * Minimum viable viewport (CSS px) for this primitive. Toasts must remain\n * legible on a 320 CSS-px iPhone SE; below this width, the preflight\n * `data-interlace-dev` outline flags the regression in development.\n */\nexport const MIN_VIEWPORT = 320 as const;\n\n// ─────────────────────────────────────────────────────────────────\n// Tone variants — 4px left accent strip + matching outline ring on\n// keyboard focus. The strip is the ONLY tone-coloured channel on the\n// surface (the body stays neutral `bg-card`) so the tone reads as a\n// secondary signal beside the consumer-supplied icon, never as the\n// sole carrier of meaning (DESIGN_PRINCIPLES #9).\n// ─────────────────────────────────────────────────────────────────\nconst toastVariants = cva(\n  [\n    'pointer-events-auto relative flex w-full items-start gap-sm overflow-hidden',\n    'border border-border bg-card text-card-foreground',\n    'rounded-md shadow-lg',\n    'p-md pl-[calc(var(--spacing-md)+0.25rem)]',\n    'before:absolute before:left-0 before:top-0 before:h-full before:w-1',\n    'before:content-[\"\"]',\n  ].join(' '),\n  {\n    variants: {\n      tone: {\n        info: 'before:bg-primary',\n        success: 'before:bg-[color:var(--interlace-success)]',\n        warning: 'before:bg-[color:var(--interlace-warning)]',\n        danger: 'before:bg-destructive',\n      },\n    },\n    defaultVariants: {\n      tone: 'info',\n    },\n  },\n);\n\n// ─────────────────────────────────────────────────────────────────\n// Animation classes — slide-from-right + fade. Reduced-motion path\n// strips the translate so users get fade-only within the same 180ms\n// envelope.\n// ─────────────────────────────────────────────────────────────────\nconst ANIMATE_BASE =\n  'transition-[transform,opacity] duration-[180ms] ease-out will-change-[transform,opacity]';\nconst ANIMATE_FULL =\n  'data-[starting-style]:translate-x-full data-[starting-style]:opacity-0 data-[ending-style]:translate-x-full data-[ending-style]:opacity-0';\nconst ANIMATE_REDUCED =\n  'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0';\n\n// ─────────────────────────────────────────────────────────────────\n// ToastProvider — owns the toast manager (timer/limit/queue). Server\n// components can render the JSX tree containing a ToastProvider, but\n// the provider itself is a client surface; this file's `use client`\n// pragma scopes that boundary.\n// ─────────────────────────────────────────────────────────────────\nfunction ToastProvider(props: React.ComponentProps<typeof BaseToast.Provider>) {\n  return <BaseToast.Provider data-slot=\"toast-provider\" {...props} />;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// ToastViewport — portalled container that pins toasts to a screen\n// corner. Padding from `--spacing-md` keeps the stack clear of the\n// safe-area edges on mobile (R19 / DESIGN_PRINCIPLES #5).\n// ─────────────────────────────────────────────────────────────────\ntype ToastViewportProps = React.ComponentProps<typeof BaseToast.Viewport>;\n\nconst ToastViewport = React.forwardRef<HTMLDivElement, ToastViewportProps>(\n  function ToastViewport({ className, ...props }, ref) {\n    return (\n      <BaseToast.Portal>\n        <BaseToast.Viewport\n          ref={ref}\n          data-slot=\"toast-viewport\"\n          data-min-viewport={String(MIN_VIEWPORT)}\n          className={cn(\n            'fixed inset-x-0 bottom-0 z-50 flex w-full flex-col gap-sm p-md',\n            'sm:inset-x-auto sm:right-0 sm:bottom-0 sm:max-w-[420px]',\n            'outline-none',\n            className,\n          )}\n          {...props}\n        />\n      </BaseToast.Portal>\n    );\n  },\n);\nToastViewport.displayName = 'ToastViewport';\n\n// ─────────────────────────────────────────────────────────────────\n// Toast (root) — the styled surface. When wired through ToastProvider\n// + useToastManager, consumers pass the `toast` object Base UI hands\n// them; for static / screenshot stories the prop is optional and we\n// render a structurally-identical surface via `useRender` (a `<div>`)\n// so the visual snapshot stays deterministic without spinning up a\n// real manager.\n// ─────────────────────────────────────────────────────────────────\ntype ToastRootBaseProps = React.ComponentProps<typeof BaseToast.Root>;\n\ntype ToastProps = Omit<ToastRootBaseProps, 'toast'> &\n  VariantProps<typeof toastVariants> & {\n    /** Base UI toast object from `useToastManager().toasts[i]`. Optional for static render. */\n    toast?: ToastRootBaseProps['toast'];\n  };\n\nconst Toast = React.forwardRef<HTMLDivElement, ToastProps>(function Toast(\n  { className, tone = 'info', toast, ...props },\n  ref,\n) {\n  const reduceMotion = useReducedMotion();\n  const animation = cn(\n    ANIMATE_BASE,\n    reduceMotion ? ANIMATE_REDUCED : ANIMATE_FULL,\n  );\n\n  const sharedProps = {\n    'data-slot': 'toast',\n    'data-min-viewport': String(MIN_VIEWPORT),\n    'data-tone': tone ?? undefined,\n    className: cn(toastVariants({ tone }), animation, className),\n    ...props,\n  } as const;\n\n  // When `toast` is wired (production path), delegate to Base UI Root so the\n  // manager owns lifecycle / focus / swipe / aria-live.\n  if (toast) {\n    return <BaseToast.Root ref={ref} toast={toast} {...sharedProps} />;\n  }\n\n  // Static path — no manager required. Render a `<div>` with the same\n  // surface so screenshot stories stay deterministic. Base UI's Root\n  // allows `style` to be a `(state) => CSSProperties` callback; a plain\n  // `<div>` only accepts a static `CSSProperties`, so we narrow here.\n  // ToastStaticCtx signals to sub-parts (Title, Description, Close) that\n  // they should also render as plain HTML — Base UI's Title/Description/\n  // Close require Toast.Root context which doesn't exist in the static path.\n  const { style: rawStyle, ...divProps } = sharedProps as typeof sharedProps & {\n    style?:\n      | React.CSSProperties\n      | ((state: unknown) => React.CSSProperties | undefined);\n  };\n  const style = typeof rawStyle === 'function' ? undefined : rawStyle;\n  return (\n    <ToastStaticCtx.Provider value={true}>\n      <div ref={ref} {...divProps} style={style} />\n    </ToastStaticCtx.Provider>\n  );\n});\nToast.displayName = 'Toast';\n\n// ─────────────────────────────────────────────────────────────────\n// ToastTitle / ToastDescription — typography parts. Title carries the\n// document outline ARIA, description is muted-foreground per the\n// reading-vs-UI typography contract.\n// ─────────────────────────────────────────────────────────────────\ntype ToastTitleProps = React.ComponentProps<typeof BaseToast.Title>;\n\nconst ToastTitle = React.forwardRef<HTMLHeadingElement, ToastTitleProps>(\n  function ToastTitle({ className, ...props }, ref) {\n    const isStatic = React.useContext(ToastStaticCtx);\n    const cls = cn(\n      'font-body text-ui font-semibold text-card-foreground',\n      className,\n    );\n    if (isStatic) {\n      return (\n        <strong\n          ref={ref as React.Ref<HTMLElement>}\n          data-slot=\"toast-title\"\n          className={cls}\n          {...(props as React.HTMLAttributes<HTMLElement>)}\n        />\n      );\n    }\n    return (\n      <BaseToast.Title\n        ref={ref}\n        data-slot=\"toast-title\"\n        className={cls}\n        {...props}\n      />\n    );\n  },\n);\nToastTitle.displayName = 'ToastTitle';\n\ntype ToastDescriptionProps = React.ComponentProps<typeof BaseToast.Description>;\n\nconst ToastDescription = React.forwardRef<\n  HTMLParagraphElement,\n  ToastDescriptionProps\n>(function ToastDescription({ className, ...props }, ref) {\n  const isStatic = React.useContext(ToastStaticCtx);\n  const cls = cn('font-body text-ui-sm text-muted-foreground', className);\n  if (isStatic) {\n    return (\n      <p\n        ref={ref}\n        data-slot=\"toast-description\"\n        className={cls}\n        {...(props as React.HTMLAttributes<HTMLParagraphElement>)}\n      />\n    );\n  }\n  return (\n    <BaseToast.Description\n      ref={ref}\n      data-slot=\"toast-description\"\n      className={cls}\n      {...props}\n    />\n  );\n});\nToastDescription.displayName = 'ToastDescription';\n\n// ─────────────────────────────────────────────────────────────────\n// ToastClose — the dismiss affordance. Lucide X icon (R13: outlined\n// iconography only) inside a Base UI Close that owns the focus +\n// keyboard contract.\n// ─────────────────────────────────────────────────────────────────\ntype ToastCloseProps = React.ComponentProps<typeof BaseToast.Close>;\n\nconst ToastClose = React.forwardRef<HTMLButtonElement, ToastCloseProps>(\n  function ToastClose({ className, children, ...props }, ref) {\n    const isStatic = React.useContext(ToastStaticCtx);\n    const cls = cn(\n      'ml-auto inline-flex size-6 shrink-0 items-center justify-center rounded-sm',\n      'text-muted-foreground hover:text-foreground',\n      'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50',\n      'transition-colors',\n      className,\n    );\n    const content = children ?? (\n      <>\n        <XIcon className=\"size-4\" aria-hidden />\n        <span className=\"sr-only\">Close</span>\n      </>\n    );\n    if (isStatic) {\n      return (\n        <button\n          ref={ref}\n          type=\"button\"\n          data-slot=\"toast-close\"\n          aria-label=\"Close\"\n          className={cls}\n          {...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}\n        >\n          {content}\n        </button>\n      );\n    }\n    return (\n      <BaseToast.Close\n        ref={ref}\n        data-slot=\"toast-close\"\n        aria-label=\"Close\"\n        className={cls}\n        {...props}\n      >\n        {content}\n      </BaseToast.Close>\n    );\n  },\n);\nToastClose.displayName = 'ToastClose';\n\n// ─────────────────────────────────────────────────────────────────\n// ToastTrigger — convenience trigger that, on click, dispatches a\n// toast through Base UI's manager. Lives here (rather than in the\n// story) so consumers get a typed, ref-forwarding, slot-renderable\n// trigger out of the box. Mirrors Button's `render` prop seam (R12)\n// so a consumer can swap the rendered element for a Button without\n// double-wrapping.\n// ─────────────────────────────────────────────────────────────────\ntype ToastTriggerProps = Omit<React.ComponentProps<'button'>, 'title'> & {\n  /** Toast title surfaced to assistive tech. */\n  title?: React.ReactNode;\n  /** Toast body text. */\n  description?: React.ReactNode;\n  /** Tone forwarded to Toast root via `type` so consumers can branch on it. */\n  tone?: NonNullable<VariantProps<typeof toastVariants>['tone']>;\n  /** Replace the rendered element (e.g. `<Button>`). Same seam as Button. */\n  render?: useRender.RenderProp;\n};\n\nfunction ToastTrigger({\n  className,\n  title,\n  description,\n  tone = 'info',\n  render,\n  onClick,\n  ...props\n}: ToastTriggerProps) {\n  const manager = BaseToast.useToastManager();\n\n  const handleClick = React.useCallback(\n    (event: React.MouseEvent<HTMLButtonElement>) => {\n      onClick?.(event);\n      if (event.defaultPrevented) return;\n      manager.add({ title, description, type: tone });\n    },\n    // `event` is this callback's own parameter (not a free variable) and\n    // `type` is an object-literal key in `manager.add({ ..., type: tone })`\n    // above, not an identifier reference — neither belongs in the\n    // dependency array.\n    // eslint-disable-next-line react-features/hooks-exhaustive-deps\n    [manager, onClick, title, description, tone],\n  );\n\n  const element = useRender({\n    // `<button type=\"button\" />` here is only a render-shape hint for\n    // useRender; the consumer's children / aria-label reach the DOM via\n    // the `props` object below, not as JSX children of this element.\n    // eslint-disable-next-line react-a11y/control-has-associated-label, react-a11y/no-missing-aria-labels\n    render: render ?? <button type=\"button\" />,\n    props: {\n      'data-slot': 'toast-trigger',\n      'data-tone': tone,\n      className: cn(className),\n      onClick: handleClick,\n      ...props,\n    },\n  });\n\n  return element;\n}\n\nexport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastTrigger,\n  ToastViewport,\n  toastVariants,\n};\nexport type {\n  ToastCloseProps,\n  ToastDescriptionProps,\n  ToastProps,\n  ToastTitleProps,\n  ToastTriggerProps,\n  ToastViewportProps,\n};\n"
    }
  ]
}
