{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"toast","title":"Toast","description":"Transient status messages managed through a scoped provider.","dependencies":["@base-ui/react@1.7.0","lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/button","@siteplane/spinner"],"files":[{"path":"packages/ui/src/components/toast/toast.tsx","content":"\"use client\";\n\nimport { Toast } from \"@base-ui/react/toast\";\nimport {\n  CircleAlertIcon,\n  CircleCheckIcon,\n  InfoIcon,\n  TriangleAlertIcon,\n  XIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { useSiteplaneMotionPreset } from \"../../system/motion/siteplane-motion-provider\";\nimport { cn, type WithStringClassName } from \"../../system/utils/cn\";\nimport { Button } from \"../button\";\nimport { KitButton } from \"../button/button-internal\";\nimport { Spinner } from \"../spinner\";\nimport { useToastStack } from \"./toast-stack\";\nimport { toastDismissClassName, toastSurfaceClassName } from \"./toast-surface\";\n\nimport \"./toast.css\";\n\n// A loading toast shows the kit Spinner instead of an icon of its own.\nconst TOAST_ICONS = {\n  error: CircleAlertIcon,\n  info: InfoIcon,\n  success: CircleCheckIcon,\n  warning: TriangleAlertIcon,\n} as const;\n\n/**\n * The content of every toast, the default one and the content `notify()`\n * passes: it fades out as the toast steps behind the front one and back in\n * when the stack expands (toast.css owns the fade), and a toast behind takes\n * no pointer until then.\n */\nconst toastContentClassName =\n  \"sp-toast-content-motion pointer-events-auto data-behind:not-data-expanded:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100\";\n\ntype SwipeDirection = \"up\" | \"down\" | \"left\" | \"right\";\n\nexport type ToastData = {\n  content?: React.ReactNode;\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\")) return [verticalDirection];\n  if (position.includes(\"left\")) return [\"left\", verticalDirection];\n  return [\"right\", verticalDirection];\n}\n\nfunction upsertReplayClassName(toast: {\n  type?: string | undefined;\n  updateKey?: number | undefined;\n}): string | undefined {\n  const key = toast.updateKey ?? 0;\n  if (key <= 0) return undefined;\n  const parity = key % 2 === 0 ? \"even\" : \"odd\";\n  return toast.type === \"error\"\n    ? `animate-toast-error-${parity}`\n    : `animate-toast-success-${parity}`;\n}\n\nfunction Toasts({\n  dismissLabel,\n  manager,\n  position,\n  portalProps,\n}: {\n  dismissLabel: string;\n  manager: object | undefined;\n  position: ToastPosition;\n  portalProps?:\n    WithStringClassName<React.ComponentProps<typeof Toast.Portal>> | undefined;\n}): React.ReactElement {\n  const { toasts } = Toast.useToastManager();\n  const swipeDirection = getSwipeDirection(position);\n  // Stacks of other providers at this position take the edge first when they\n  // received a toast later; this one stands back by their room.\n  const [viewport, setViewport] = React.useState<HTMLDivElement | null>(null);\n  const { mirrored, offset } = useToastStack(\n    position,\n    manager,\n    toasts,\n    viewport,\n  );\n  // The viewport portals out of a SiteplaneMotionProvider, so it carries the\n  // preset itself for the reduced-motion rules in toast.css.\n  const motionPreset = useSiteplaneMotionPreset();\n\n  return (\n    <Toast.Portal\n      className=\"sp-toast-portal\"\n      data-slot=\"toast-portal\"\n      {...portalProps}\n    >\n      <Toast.Viewport\n        ref={setViewport}\n        className={cn(\n          \"sp-toast-viewport\",\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          \"data-[position*=top]:top-(--toast-inset)\",\n          \"data-[position*=bottom]:bottom-(--toast-inset)\",\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          \"data-[position*=top]:transform-[translateY(var(--toast-stack-offset,0px))]\",\n          \"data-[position*=bottom]:transform-[translateY(calc(var(--toast-stack-offset,0px)*-1))]\",\n          \"data-mirrored:invisible\",\n        )}\n        data-mirrored={mirrored ? \"\" : undefined}\n        data-position={position}\n        data-slot=\"toast-viewport\"\n        data-sp-motion={motionPreset}\n        data-stacked={toasts.length > 0 ? \"\" : undefined}\n        style={\n          offset > 0\n            ? ({ \"--toast-stack-offset\": `${offset}px` } as React.CSSProperties)\n            : undefined\n        }\n      >\n        {toasts.map((toast) => {\n          const Icon =\n            toast.type && toast.type !== \"loading\"\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                toastSurfaceClassName,\n                \"absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full select-none sp-toast-motion data-expanded:bg-(--sp-rw-surface-color) dark:data-expanded:bg-(--sp-rw-surface-color)\",\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                \"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                \"[--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                \"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                \"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                \"data-limited:opacity-0\",\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                \"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                \"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                \"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              {toastData?.content ? (\n                <Toast.Content\n                  className={cn(\n                    toastContentClassName,\n                    \"overflow-hidden rounded-[inherit]\",\n                  )}\n                >\n                  <Toast.Title className=\"sr-only\" />\n                  {toastData.content}\n                </Toast.Content>\n              ) : (\n                <Toast.Content\n                  className={cn(\n                    toastContentClassName,\n                    \"flex items-center justify-between gap-1.5 overflow-hidden py-3 ps-3.5 pe-10 text-sm\",\n                  )}\n                >\n                  {/* The text column may shrink, so a long title or\n                      description wraps instead of pushing the action out\n                      (rulebook K5). */}\n                  <div className=\"flex min-w-0 gap-2\">\n                    {(Icon || toast.type === \"loading\") && (\n                      <div\n                        className=\"sp-toast-icon [&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\"\n                        data-slot=\"toast-icon\"\n                      >\n                        {Icon ? (\n                          <Icon className=\"sp-spinner-motion in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning\" />\n                        ) : (\n                          <Spinner aria-hidden=\"true\" className=\"opacity-80\" />\n                        )}\n                      </div>\n                    )}\n\n                    <div className=\"flex min-w-0 flex-col gap-0.5\">\n                      <Toast.Title\n                        className=\"sp-toast-title font-(--sp-rw-font-weight-label) wrap-anywhere\"\n                        data-slot=\"toast-title\"\n                      />\n                      <Toast.Description\n                        className=\"sp-toast-description text-muted-foreground wrap-anywhere\"\n                        data-slot=\"toast-description\"\n                      />\n                    </div>\n                  </div>\n                  {toast.actionProps && (\n                    <Toast.Action\n                      className=\"sp-toast-action\"\n                      data-slot=\"toast-action\"\n                      render={<Button size=\"xs\" />}\n                    >\n                      {toast.actionProps.children}\n                    </Toast.Action>\n                  )}\n                </Toast.Content>\n              )}\n              <ToastCloseButton label={dismissLabel} />\n            </Toast.Root>\n          );\n        })}\n      </Toast.Viewport>\n    </Toast.Portal>\n  );\n}\n\nfunction ToastCloseButton({ label }: { label: string }): React.ReactElement {\n  return (\n    <Toast.Close\n      aria-label={label}\n      className={cn(\"sp-toast-close\", toastDismissClassName)}\n      data-slot=\"toast-close\"\n      render={\n        <KitButton profile=\"overlay-dismiss\" shape=\"square\" variant=\"ghost\" />\n      }\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  /** The accessible name of each toast's dismiss button. Default: \"Dismiss toast\" */\n  dismissLabel?: string;\n  position?: ToastPosition;\n  portalProps?: WithStringClassName<React.ComponentProps<typeof Toast.Portal>>;\n}\n\nexport function ToastProvider({\n  children,\n  dismissLabel = \"Dismiss toast\",\n  position = \"bottom-right\",\n  portalProps,\n  ...props\n}: ToastProviderProps): React.ReactElement {\n  const manager = \"toastManager\" in props ? props.toastManager : toastManager;\n  return (\n    <Toast.Provider toastManager={toastManager} {...props}>\n      {children}\n      <Toasts\n        dismissLabel={dismissLabel}\n        manager={manager}\n        portalProps={portalProps}\n        position={position}\n      />\n    </Toast.Provider>\n  );\n}\n\nexport { Toast as ToastPrimitive };\n","type":"registry:ui","target":"~/src/siteplane/components/toast/toast.tsx"},{"path":"packages/ui/src/components/toast/toast.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* A toast and a Notification read the kit's theme and motion tokens, so a\n   * re-theme reaches them. Their surface publishes its colour like every\n   * floating surface: the fill darkens from it per stack position, and the\n   * status ring of an avatar takes it. */\n  .sp-toast-replay-motion {\n    --sp-rw-surface-color: var(--sp-rw-bg-secondary);\n  }\n\n  /* The content of every toast, the default one and that of notify(), fades\n   * in and out as the toast steps behind the front one and back. */\n  :where(.sp-toast-content-motion) {\n    transition-property: opacity;\n    transition-duration: var(--motion-duration-moderate);\n    transition-timing-function: var(--motion-ease-standard);\n  }\n\n  /* A toast that is updated in place replays its type: a success pulses over\n   * the slow step, and an error shakes a little quicker, in seven eighths of\n   * it. */\n  .animate-toast-success-odd,\n  .animate-toast-success-even {\n    animation: toast-success var(--motion-duration-slow)\n      var(--motion-ease-toast-replay);\n  }\n\n  .animate-toast-error-odd,\n  .animate-toast-error-even {\n    animation: toast-error calc(var(--motion-duration-slow) * 0.875)\n      var(--motion-ease-toast-replay);\n  }\n\n  @keyframes toast-success {\n    0% {\n      scale: 1;\n    }\n\n    30% {\n      scale: 1.025;\n    }\n\n    60% {\n      scale: 0.99;\n    }\n\n    100% {\n      scale: 1;\n    }\n  }\n\n  @keyframes toast-error {\n    0%,\n    100% {\n      translate: 0 0;\n    }\n\n    25%,\n    75% {\n      translate: -3px 0;\n    }\n\n    50% {\n      translate: 3px 0;\n    }\n  }\n\n  /* Reduced motion is instant (M5): the replay and the spinner stop, the\n   * content no longer fades, and the stack no longer glides when a toast comes,\n   * goes or another stack makes way. */\n  @media (prefers-reduced-motion: reduce) {\n    .sp-toast-replay-motion {\n      animation: none;\n      transition: none;\n    }\n\n    .sp-toast-viewport {\n      transition: none;\n    }\n\n    .sp-toast-replay-motion .sp-spinner-motion {\n      animation: none;\n    }\n\n    :root .sp-toast-content-motion {\n      transition: none;\n    }\n  }\n\n  html[data-motion-reduce=\"true\"] .sp-toast-replay-motion,\n  [data-sp-motion=\"reduced\"] .sp-toast-replay-motion {\n    animation: none;\n    transition: none;\n  }\n\n  /* The viewport portals out of a SiteplaneMotionProvider and carries its preset. */\n  html[data-motion-reduce=\"true\"] .sp-toast-viewport,\n  [data-sp-motion=\"reduced\"] .sp-toast-viewport,\n  .sp-toast-viewport[data-sp-motion=\"reduced\"] {\n    transition: none;\n  }\n\n  html[data-motion-reduce=\"true\"] .sp-toast-replay-motion .sp-spinner-motion,\n  [data-sp-motion=\"reduced\"] .sp-toast-replay-motion .sp-spinner-motion {\n    animation: none;\n  }\n\n  html[data-motion-reduce=\"true\"] .sp-toast-content-motion,\n  [data-sp-motion=\"reduced\"] .sp-toast-content-motion {\n    transition: none;\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/toast/toast.css"},{"path":"packages/ui/src/components/toast/toast-stack.ts","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/*\n * Toast stacks that share a screen corner.\n *\n * A ToastProvider keeps its own toasts and stacks them itself. Two providers at\n * one position would draw their newest toast in the same place, one covering\n * the other completely. Every stack reports how much room it takes. The stack\n * that received a toast last sits at the edge and the others make way for it,\n * one above the other, so every front toast stays readable. A position with a\n * single stack is not moved at all.\n *\n * Providers on the same manager receive every toast twice. Only the first of\n * them is drawn; the others stay in step but are hidden, as if there were one.\n */\n\n/** `--toast-gap` and `--toast-peek` of a toast (`--spacing(3)`), in rem. */\nexport const TOAST_GAP_REM = 0.75;\nexport const TOAST_PEEK_REM = 0.75;\n\n/** What a stack needs to know about one of its toasts. */\nexport interface StackToast {\n  id: string;\n  height?: number | undefined;\n  limited?: boolean | undefined;\n  transitionStatus?: \"starting\" | \"ending\" | undefined;\n}\n\n/**\n * How far a stack reaches from its screen edge, in px, counting the toasts that\n * are on show as they sit now. A collapsed stack is its front toast and a sliver\n * of every toast behind it. An expanded stack lists each toast with a gap\n * between. A toast that is closing or over the limit takes no room.\n */\nexport function stackExtent(\n  toasts: readonly StackToast[],\n  expanded: boolean,\n  rem = 16,\n): number {\n  const shown = toasts.filter(\n    (toast) => toast.transitionStatus !== \"ending\" && !toast.limited,\n  );\n  const [front] = shown;\n  if (!front) return 0;\n  if (expanded) {\n    const heights = shown.reduce((sum, toast) => sum + (toast.height ?? 0), 0);\n    return heights + (shown.length - 1) * TOAST_GAP_REM * rem;\n  }\n  return (front.height ?? 0) + (shown.length - 1) * TOAST_PEEK_REM * rem;\n}\n\n/** One stack at a position. */\ninterface Stack {\n  /** Without any toast, a stack takes the edge when it gets its first one. */\n  empty: boolean;\n  /** Which toast manager feeds it. */\n  manager: number;\n  /** Higher is later. The stack that received a toast last has the highest rank. */\n  rank: number;\n  /** When it appeared; the earliest stack of a manager is the one that is drawn. */\n  seq: number;\n  /** Its extent plus the gap to the stack behind it; 0 when nothing is on show. */\n  size: number;\n}\n\nconst corners = new Map<string, Map<string, Stack>>();\nconst managers = new WeakMap<object, number>();\nconst listeners = new Set<() => void>();\nlet lastRank = 0;\nlet lastSeq = 0;\nlet lastManager = 0;\nlet lastKey = 0;\n\n/** A number for a toast manager; 0 for none. The same manager always gets the same number. */\nexport function managerId(manager: object | undefined): number {\n  if (!manager) return 0;\n  let id = managers.get(manager);\n  if (id === undefined) managers.set(manager, (id = ++lastManager));\n  return id;\n}\n\nfunction subscribe(listener: () => void): () => void {\n  listeners.add(listener);\n  return () => {\n    listeners.delete(listener);\n  };\n}\n\nfunction emit(): void {\n  for (const listener of listeners) listener();\n}\n\n/** What a stack tells the others each time its toasts change. */\nexport interface StackReport {\n  empty: boolean;\n  extent: number;\n  gap: number;\n  manager: number;\n  /** A toast was added since the last report. */\n  received: boolean;\n}\n\nexport function publishStack(\n  position: string,\n  key: string,\n  { empty, extent, gap, manager, received }: StackReport,\n): void {\n  let corner = corners.get(position);\n  if (!corner) corners.set(position, (corner = new Map()));\n  const previous = corner.get(key);\n  const rank = received ? ++lastRank : (previous?.rank ?? 0);\n  const size = extent > 0 ? extent + gap : 0;\n  if (\n    previous &&\n    previous.empty === empty &&\n    previous.manager === manager &&\n    previous.rank === rank &&\n    previous.size === size\n  )\n    return;\n  corner.set(key, {\n    empty,\n    manager,\n    rank,\n    seq: previous?.seq ?? ++lastSeq,\n    size,\n  });\n  emit();\n}\n\nexport function retireStack(position: string, key: string): void {\n  const corner = corners.get(position);\n  if (!corner?.delete(key)) return;\n  if (corner.size === 0) corners.delete(position);\n  emit();\n}\n\n/** Whether an earlier stack at the position is fed by the same manager. */\nfunction isMirror(\n  corner: ReadonlyMap<string, Stack>,\n  key: string,\n  stack: Stack,\n): boolean {\n  if (stack.manager === 0) return false;\n  for (const [other, candidate] of corner)\n    if (\n      other !== key &&\n      candidate.manager === stack.manager &&\n      candidate.seq < stack.seq\n    )\n      return true;\n  return false;\n}\n\n/** Whether the stack only repeats what an earlier stack on its manager shows. */\nexport function isMirrored(position: string, key: string): boolean {\n  const corner = corners.get(position);\n  const own = corner?.get(key);\n  return Boolean(corner && own && isMirror(corner, key, own));\n}\n\n/**\n * How far the stack stands back from the edge: the room of every stack that is\n * later. A stack without toasts stays at the edge, so that its first toast\n * enters there instead of gliding in from behind the others.\n */\nexport function offsetOf(position: string, key: string): number {\n  const corner = corners.get(position);\n  const own = corner?.get(key);\n  if (!corner || !own || own.empty) return 0;\n  let offset = 0;\n  for (const [other, stack] of corner)\n    if (\n      other !== key &&\n      stack.rank > own.rank &&\n      !isMirror(corner, other, stack)\n    )\n      offset += stack.size;\n  return offset;\n}\n\nfunction useHasAttribute(element: Element | null, name: string): boolean {\n  const [has, setHas] = React.useState(false);\n  React.useEffect(() => {\n    if (!element) return;\n    const read = () => setHas(element.hasAttribute(name));\n    read();\n    const observer = new MutationObserver(read);\n    observer.observe(element, { attributeFilter: [name], attributes: true });\n    return () => observer.disconnect();\n  }, [element, name]);\n  return has;\n}\n\n/**\n * Where a stack stands among the stacks at its position: how far it stands back\n * from the screen edge, in px, so that it clears the stacks that received a toast\n * later, and whether it is only a repeat of the stack of its own manager. It\n * follows the stack's own toasts and whether the viewport is expanded (Base UI\n * sets `data-expanded`).\n */\nexport function useToastStack(\n  position: string,\n  manager: object | undefined,\n  toasts: readonly StackToast[],\n  viewport: Element | null,\n): { mirrored: boolean; offset: number } {\n  // Not `useId`: separate React roots on one page can hand out the same id.\n  const [key] = React.useState(() => `stack-${++lastKey}`);\n  const expanded = useHasAttribute(viewport, \"data-expanded\");\n  const known = React.useRef(new Set<string>());\n\n  React.useLayoutEffect(() => {\n    const rem =\n      Number.parseFloat(getComputedStyle(document.documentElement).fontSize) ||\n      16;\n    // New toasts enter at the front of the list; one that leaves does not\n    // change which toast was added last.\n    const front = toasts[0]?.id;\n    const received = front !== undefined && !known.current.has(front);\n    known.current = new Set(toasts.map((toast) => toast.id));\n    publishStack(position, key, {\n      empty: toasts.length === 0,\n      extent: stackExtent(toasts, expanded, rem),\n      gap: TOAST_GAP_REM * rem,\n      manager: managerId(manager),\n      received,\n    });\n  }, [position, key, manager, toasts, expanded]);\n\n  React.useLayoutEffect(\n    () => () => {\n      known.current = new Set();\n      retireStack(position, key);\n    },\n    [position, key],\n  );\n\n  const offset = React.useSyncExternalStore(\n    subscribe,\n    () => offsetOf(position, key),\n    () => 0,\n  );\n  const mirrored = React.useSyncExternalStore(\n    subscribe,\n    () => isMirrored(position, key),\n    () => false,\n  );\n  return { mirrored, offset };\n}\n","type":"registry:ui","target":"~/src/siteplane/components/toast/toast-stack.ts"},{"path":"packages/ui/src/components/toast/toast-surface.ts","content":"// The chrome that Toast and the static Notification share. Kit internals,\n// imported by file path; neither is part of the public Toast API.\n\n/**\n * The toast surface: the kit's floating surface, radius and fill. The static\n * Notification renders the same chrome, so a preview and the toast it previews\n * cannot drift apart. The `sp-toast-surface` rule in design-rework.css\n * draws the borderless floating surface of Popover and Dialog: one edge from\n * the floating shadow, no border (rulebook B7). The fill is the surface colour\n * that toast.css publishes and mixes in black per stack position; outside a\n * toast there is no stack position and the fill is the plain surface colour.\n * The radius is the control radius token, not `rounded-lg`: a toast portals out\n * of the provider while the preview sits inside it, and the provider rescales\n * `--radius`, which would make the two differ.\n */\nexport const toastSurfaceClassName =\n  \"sp-toast-surface sp-toast-replay-motion relative rounded-(--sp-rw-radius-control) bg-[color-mix(in_srgb,var(--sp-rw-surface-color),var(--color-black)_calc(1%*max(0,var(--toast-index,0))))] dark:bg-[color-mix(in_srgb,var(--sp-rw-surface-color),var(--color-black)_calc(6%*max(0,var(--toast-index,0))))]\";\n\n/**\n * Where the dismiss control sits on that surface: 8 px inside the top-end\n * corner, as the close of every overlay (rulebook B5). Button's\n * overlay-dismiss profile owns its 24 px box, its 16 px glyph, its colour and\n * its hover; the surface owns only the corner. Text beside it ends 8 px before\n * it, 40 px from the edge (`pe-10`).\n */\nexport const toastDismissClassName = \"absolute end-2 top-2 z-10\";\n","type":"registry:ui","target":"~/src/siteplane/components/toast/toast-surface.ts"},{"path":"packages/ui/src/components/toast/index.ts","content":"export * from \"./toast\";\n","type":"registry:ui","target":"~/src/siteplane/components/toast/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/toast.md","categories":["Feedback"],"meta":{"avoidWhen":["Avoid for an error that blocks a task; keep it beside the field or use Alert Dialog.","Avoid as the only place for a persistent form error."],"description":"Transient status messages managed through a scoped provider.","knownLimitations":["Each provider keeps its own toasts; with several providers at one position, the stack that received a toast last takes the edge."],"providerNotes":["Mount ToastProvider once near the application root."],"releaseStatus":"stable","searchTerms":["toast"],"useWhen":["Use for a short message after an action, such as Saved or Upload complete, that goes away by itself."]}}