{
  "name": "toast",
  "type": "registry:ui",
  "title": "Toast",
  "description": "Temporary success feedback and other brief, dismissible status messages after a real user action.",
  "registryDependencies": [
    "@siteplane/base",
    "@siteplane/button"
  ],
  "dependencies": [
    "@base-ui/react@1.5.0",
    "lucide-react@1.18.0"
  ],
  "files": [
    {
      "path": "src/components/ui/toast.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Toast } from \"@base-ui/react/toast\";\nimport {\n  CircleAlertIcon,\n  CircleCheckIcon,\n  InfoIcon,\n  LoaderCircleIcon,\n  TriangleAlertIcon,\n  XIcon,\n} from \"lucide-react\";\nimport type React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\nconst TOAST_ICONS = {\n  error: CircleAlertIcon,\n  info: InfoIcon,\n  loading: LoaderCircleIcon,\n  success: CircleCheckIcon,\n  warning: TriangleAlertIcon,\n} as const;\n\ntype SwipeDirection = \"up\" | \"down\" | \"left\" | \"right\";\n\ntype ToastData = {\n  rootProps?: Omit<\n    React.ComponentProps<typeof Toast.Root>,\n    \"children\" | \"className\" | \"swipeDirection\" | \"toast\"\n  >;\n};\n\nfunction getSwipeDirection(position: ToastPosition): SwipeDirection[] {\n  const verticalDirection: SwipeDirection = position.startsWith(\"top\")\n    ? \"up\"\n    : \"down\";\n\n  if (position.includes(\"center\")) {\n    return [verticalDirection];\n  }\n\n  if (position.includes(\"left\")) {\n    return [\"left\", verticalDirection];\n  }\n\n  return [\"right\", verticalDirection];\n}\n\nfunction upsertReplayClassName(toast: {\n  type?: string | undefined;\n  updateKey?: number | undefined;\n}): string | undefined {\n  const k = toast.updateKey ?? 0;\n  if (k <= 0) return undefined;\n  const isEven = k % 2 === 0;\n  if (toast.type === \"error\") {\n    return isEven ? \"animate-toast-error-even\" : \"animate-toast-error-odd\";\n  }\n  return isEven ? \"animate-toast-success-even\" : \"animate-toast-success-odd\";\n}\n\nfunction Toasts({\n  position,\n  portalProps,\n}: {\n  position: ToastPosition;\n  portalProps?: React.ComponentProps<typeof Toast.Portal> | undefined;\n}): React.ReactElement {\n  const { toasts } = Toast.useToastManager();\n  const swipeDirection = getSwipeDirection(position);\n\n  return (\n    <Toast.Portal data-slot=\"toast-portal\" {...portalProps}>\n      <Toast.Viewport\n        className={cn(\n          \"fixed z-60 mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]\",\n          // Vertical positioning\n          \"data-[position*=top]:top-(--toast-inset)\",\n          \"data-[position*=bottom]:bottom-(--toast-inset)\",\n          // Horizontal positioning\n          \"data-[position*=left]:left-(--toast-inset)\",\n          \"data-[position*=right]:right-(--toast-inset)\",\n          \"data-[position*=center]:left-1/2 data-[position*=center]:-translate-x-1/2\",\n        )}\n        data-position={position}\n        data-slot=\"toast-viewport\"\n      >\n        {toasts.map((toast) => {\n          const Icon = toast.type\n            ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]\n            : null;\n          const toastData = toast.data as ToastData | undefined;\n\n          return (\n            <Toast.Root\n              key={toast.id}\n              className={cn(\n                \"siteplane-toast-replay-motion absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full select-none rounded-lg border bg-[color-mix(in_srgb,var(--popover),var(--color-black)_calc(1%*max(0,var(--toast-index,0))))] not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 [transition:transform_var(--motion-duration-linger)_var(--motion-ease-standard),opacity_var(--motion-duration-linger),height_var(--motion-duration-fast),background-color_var(--motion-duration-linger)] before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:shadow-[0_1px_--theme(--color-black/4%)] data-expanded:bg-popover dark:bg-[color-mix(in_srgb,var(--popover),var(--color-black)_calc(6%*max(0,var(--toast-index,0))))] dark:data-expanded:bg-popover dark:before:shadow-[0_-1px_--theme(--color-white/6%)]\",\n                // Base positioning using data-position\n                \"data-[position*=right]:right-0 data-[position*=right]:left-auto\",\n                \"data-[position*=left]:right-auto data-[position*=left]:left-0\",\n                \"data-[position*=center]:right-0 data-[position*=center]:left-0\",\n                \"data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-[50%_calc(50%-50%*min(var(--toast-index,0),1))]\",\n                \"data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-[50%_calc(50%+50%*min(var(--toast-index,0),1))]\",\n                // Gap fill for hover\n                \"after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full\",\n                \"data-[position*=top]:after:top-full\",\n                \"data-[position*=bottom]:after:bottom-full\",\n                // Define some variables\n                \"[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-exit-scale:clamp(0.82,calc(var(--toast-scale)-0.04),1)] [--toast-shrink:calc(1-var(--toast-scale))]\",\n                // Define offset-y variable\n                \"data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]\",\n                \"data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]\",\n                // Default state transform\n                \"data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]\",\n                \"data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]\",\n                // Limited state\n                \"data-limited:opacity-0\",\n                // Expanded state\n                \"data-expanded:h-(--toast-height)\",\n                \"data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]\",\n                // Starting and ending animations\n                \"data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]\",\n                \"data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]\",\n                \"data-ending-style:opacity-0\",\n                // Ending animations: close/timeout fades back into the stack; swipe remains direction-aware.\n                \"data-[position*=top]:data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-exit-scale))]\",\n                \"data-[position*=bottom]:data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-exit-scale))]\",\n                \"data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]\",\n                \"data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]\",\n                \"data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]\",\n                \"data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]\",\n                // Ending animations (expanded)\n                \"data-position:data-expanded:data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))_scale(var(--toast-exit-scale))]\",\n                \"data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]\",\n                \"data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]\",\n                \"data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]\",\n                \"data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]\",\n                upsertReplayClassName(toast),\n              )}\n              {...toastData?.rootProps}\n              data-position={position}\n              swipeDirection={swipeDirection}\n              toast={toast}\n            >\n              <Toast.Content className=\"pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden py-3 ps-3.5 pe-9 text-sm transition-opacity duration-(--motion-duration-route) data-behind:not-data-expanded:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100\">\n                <div className=\"flex gap-2\">\n                  {Icon && (\n                    <div\n                      className=\"[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\"\n                      data-slot=\"toast-icon\"\n                    >\n                      <Icon className=\"siteplane-spinner-motion in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80\" />\n                    </div>\n                  )}\n\n                  <div className=\"flex flex-col gap-0.5\">\n                    <Toast.Title\n                      className=\"font-medium\"\n                      data-slot=\"toast-title\"\n                    />\n                    <Toast.Description\n                      className=\"text-muted-foreground\"\n                      data-slot=\"toast-description\"\n                    />\n                  </div>\n                </div>\n                {toast.actionProps && (\n                  <Toast.Action\n                    className={buttonVariants({ size: \"xs\" })}\n                    data-slot=\"toast-action\"\n                  >\n                    {toast.actionProps.children}\n                  </Toast.Action>\n                )}\n              </Toast.Content>\n              <ToastCloseButton />\n            </Toast.Root>\n          );\n        })}\n      </Toast.Viewport>\n    </Toast.Portal>\n  );\n}\n\nfunction ToastCloseButton(): React.ReactElement {\n  return (\n    <Toast.Close\n      aria-label=\"Dismiss toast\"\n      className=\"absolute top-1.5 right-1.5 z-10 inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent text-muted-foreground opacity-80 outline-none transition-[color,background-color,box-shadow,opacity] pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0\"\n      data-slot=\"toast-close\"\n    >\n      <XIcon />\n    </Toast.Close>\n  );\n}\n\nexport const toastManager: ReturnType<typeof Toast.createToastManager> =\n  Toast.createToastManager();\n\nexport type ToastPosition =\n  | \"top-left\"\n  | \"top-center\"\n  | \"top-right\"\n  | \"bottom-left\"\n  | \"bottom-center\"\n  | \"bottom-right\";\n\nexport interface ToastProviderProps extends Toast.Provider.Props {\n  position?: ToastPosition;\n  portalProps?: React.ComponentProps<typeof Toast.Portal>;\n}\n\nexport function ToastProvider({\n  children,\n  position = \"bottom-right\",\n  portalProps,\n  ...props\n}: ToastProviderProps): React.ReactElement {\n  return (\n    <Toast.Provider toastManager={toastManager} {...props}>\n      {children}\n      <Toasts portalProps={portalProps} position={position} />\n    </Toast.Provider>\n  );\n}\n\nexport { Toast as ToastPrimitive };\n"
    }
  ],
  "categories": [
    "feedback"
  ],
  "docs": "# Toast\n\n> Siteplane UI release `0.1.1`.\n\n## Public Purpose\n\nUse Toast for brief feedback after a real user action when the message does not require a blocking decision.\n\n## Import\n\n```tsx\nimport { ToastProvider, toastManager } from \"@/components/ui/toast\";\n```\n\n## Public API\n\n- Mount one `ToastProvider` at the app root. Its `position` supports `top-left`, `top-center`, `top-right`, `bottom-left`, `bottom-center` and `bottom-right`.\n- Use `toastManager.add`, `toastManager.update`, `toastManager.close` and `toastManager.promise(promise, { loading, success, error })` from anywhere below the provider.\n- Position controls viewport corner, stack direction, entry and exit direction, and the permitted swipe direction.\n\n## Public Motion\n\n- Toast stacking, height, opacity, transform and swipe exit use the central Siteplane motion tokens.\n- The forced `reducedMotionPreset` shortens the tokenized stack transitions. Provider and operating-system reduced-motion modes stop both the continuous loading-icon spin and the cosmetic success/error update replay; messages and dismissal remain functional.\n\n## Public Accessibility\n\n- Keep messages concise and announce the result of a real action; do not hide critical instructions only in a transient Toast.\n- Every Toast remains manually dismissible in addition to its timeout.\n- Choose wording and urgency that match the result, and avoid firing repeated messages for routine state changes.\n\n## Public Agent Guidance\n\n- Mount exactly one app-wide provider and set its position deliberately.\n- Use `toastManager.promise` for a promise lifecycle instead of manually recreating loading, success and error updates.\n- Use Alert, inline validation or Dialog for information that must remain visible or requires a decision.\n\n## Public Links\n\n- [Documentation and preview](https://ui.siteplane.io/docs/components/feedback/toast)\n\n- [Registry source](https://ui.siteplane.io/r/toast.json)\n",
  "meta": {
    "siteplane:agentContractVersion": 1,
    "siteplane:docs": "https://ui.siteplane.io/docs/components/feedback/toast",
    "siteplane:releaseVersion": "0.1.1",
    "siteplane:sourceOwned": true
  },
  "devDependencies": []
}
