{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sonner-rich",
  "title": "Rich sonner",
  "description": "Styled sonner: the Rich set. 5 decorative sonner variations (TitleToaster, AvatarToaster, MediaToaster, DescriptionToaster, StackedToaster) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/sonner-rich.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AlertTriangle, CheckCircle, ImageIcon, Info, X } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Styled sonner - rich family: 5 self-contained toasters. NO sonner package and NO\n   portal - each export renders a demo trigger button; clicking it pushes a toast\n   onto a LOCAL stack (a useState array), and it appears in the bottom-right corner\n   (fixed bottom-right, z-50). The difference: richer content - a title, an avatar, a\n   media thumbnail, a long description and a grouped list. Multiple toasts stack on\n   top of each other and disappear on a timeout or via the close button. Color comes\n   ONLY from tokens, via alpha color-mix. Deterministic: the ids come from a ref\n   counter (NO Date.now/Math.random) and the timeout is fixed. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nexport type StyledTone = \"success\" | \"info\" | \"warning\" | \"danger\"\n\n/* Toast genisligi size'a bagli. Zengin icerik biraz daha genis taban ister. */\nconst toastWidth: Record<StyledSize, string> = {\n  sm: \"w-64\",\n  md: \"w-72\",\n  lg: \"w-80\",\n  xl: \"w-96\",\n}\n\n/* Tone -> ikon rengi (semantic token). */\nconst toneIconColor: Record<StyledTone, string> = {\n  success: \"text-success\",\n  info: \"text-info\",\n  warning: \"text-warning-soft-foreground\",\n  danger: \"text-danger\",\n}\n\n/* Tone -> ikon bileseni. */\nconst toneIcon: Record<StyledTone, React.ComponentType<{ className?: string }>> = {\n  success: CheckCircle,\n  info: Info,\n  warning: AlertTriangle,\n  danger: X,\n}\n\n/* Tone -> yumusak yuzey (soft token cifti). */\nconst toneSoft: Record<StyledTone, string> = {\n  success: \"bg-success-soft text-success-soft-foreground\",\n  info: \"bg-info-soft text-info-soft-foreground\",\n  warning: \"bg-warning-soft text-warning-soft-foreground\",\n  danger: \"bg-danger-soft text-danger-soft-foreground\",\n}\n\nconst triggerBtn =\n  \"inline-flex h-9 shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-lg border border-border bg-secondary px-4 text-sm font-medium text-secondary-foreground outline-none transition-colors hover:bg-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)] focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst closeBtn =\n  \"inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-[color-mix(in_oklab,var(--color-foreground)_8%,transparent)] hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst toastBase =\n  \"pointer-events-auto relative flex items-start gap-3 overflow-hidden rounded-xl border border-border bg-popover p-4 text-sm text-popover-foreground shadow-lg [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst stackClass =\n  \"pointer-events-none fixed bottom-0 right-0 z-50 flex flex-col items-end gap-2 p-4\"\n\n/* The auto-dismiss duration (fixed, deterministic). Rich content stays longer. */\nconst TOAST_MS = 6000\n\n/* A slide+fade enter/exit; instant under reduced-motion (opacity only). */\nfunction toastMotion(reduce: boolean | null) {\n  if (reduce) {\n    return {\n      initial: { opacity: 0 },\n      animate: { opacity: 1 },\n      exit: { opacity: 0 },\n      transition: { duration: 0.12 },\n    }\n  }\n  return {\n    initial: { opacity: 0, x: 32, scale: 0.96 },\n    animate: { opacity: 1, x: 0, scale: 1 },\n    exit: { opacity: 0, x: 32, scale: 0.96 },\n    transition: { type: \"spring\" as const, stiffness: 320, damping: 28 },\n  }\n}\n\ninterface ToastItem {\n  id: number\n}\n\n/* Shared stack logic: ref-counted id, fixed timeout, every timer cleared on\n   unmount (no setState-after-unmount). */\nfunction useToastStack() {\n  const [items, setItems] = React.useState<ToastItem[]>([])\n  const counter = React.useRef(0)\n  const timers = React.useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map())\n\n  const dismiss = React.useCallback((id: number) => {\n    setItems((prev) => prev.filter((t) => t.id !== id))\n    const timer = timers.current.get(id)\n    if (timer) {\n      clearTimeout(timer)\n      timers.current.delete(id)\n    }\n  }, [])\n\n  const push = React.useCallback(() => {\n    const id = counter.current++\n    setItems((prev) => [...prev, { id }])\n    const timer = setTimeout(() => dismiss(id), TOAST_MS)\n    timers.current.set(id, timer)\n  }, [dismiss])\n\n  React.useEffect(() => {\n    const map = timers.current\n    return () => {\n      map.forEach((t) => clearTimeout(t))\n      map.clear()\n    }\n  }, [])\n\n  return { items, push, dismiss }\n}\n\ninterface RichToasterProps {\n  className?: string\n  size?: StyledSize\n  tone?: StyledTone\n  label?: React.ReactNode\n}\n\n/* Single body: the content rendering is variant-specific. */\nfunction RichShell({\n  className,\n  size,\n  tone,\n  label,\n  bodyClass,\n  children,\n}: Required<Pick<RichToasterProps, \"size\" | \"tone\">> & {\n  className?: string\n  label?: React.ReactNode\n  bodyClass?: string\n  children: (item: ToastItem, dismiss: (id: number) => void) => React.ReactNode\n}) {\n  const reduce = useReducedMotion()\n  const { items, push, dismiss } = useToastStack()\n  const m = toastMotion(reduce)\n\n  return (\n    <div data-slot=\"styled-sonner\" className={cn(\"inline-flex\", className)}>\n      <button type=\"button\" className={triggerBtn} onClick={push}>\n        {label}\n      </button>\n      <div className={stackClass}>\n        <AnimatePresence initial={false}>\n          {items.map((t) => (\n            <motion.div\n              key={t.id}\n              role=\"status\"\n              data-tone={tone}\n              className={cn(toastBase, toastWidth[size], bodyClass)}\n              initial={m.initial}\n              animate={m.animate}\n              exit={m.exit}\n              transition={m.transition}\n              layout={!reduce}\n            >\n              {children(t, dismiss)}\n            </motion.div>\n          ))}\n        </AnimatePresence>\n      </div>\n    </div>\n  )\n}\n\n/* Sag ustte duran kapat butonu (icerik kolonu pr-6 ile yer acar). */\nfunction CornerClose({ onClick }: { onClick: () => void }) {\n  return (\n    <button\n      type=\"button\"\n      aria-label=\"Dismiss\"\n      className={cn(closeBtn, \"absolute right-2 top-2\")}\n      onClick={onClick}\n    >\n      <X />\n    </button>\n  )\n}\n\n/* ---------------------------------------------------------------- TitleToaster A tone icon plus a bold title plus a one-line message. */\nexport function TitleToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: RichToasterProps) {\n  const Icon = toneIcon[tone]\n  return (\n    <RichShell className={className} size={size} tone={tone} label={label}>\n      {(t, dismiss) => (\n        <>\n          <Icon className={cn(\"mt-0.5\", toneIconColor[tone])} />\n          <div className=\"min-w-0 flex-1 pr-6\">\n            <div className=\"font-medium tracking-tight text-foreground\">Deployment ready</div>\n            <div className=\"mt-0.5 text-muted-foreground\">Your preview is live.</div>\n          </div>\n          <CornerClose onClick={() => dismiss(t.id)} />\n        </>\n      )}\n    </RichShell>\n  )\n}\n\n/* --------------------------------------------------------------- AvatarToaster\n   Bas harf avatari + kisi adi + mesaj. */\nexport function AvatarToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: RichToasterProps) {\n  return (\n    <RichShell className={className} size={size} tone={tone} label={label}>\n      {(t, dismiss) => (\n        <>\n          <span\n            aria-hidden\n            className={cn(\n              \"inline-flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-medium\",\n              toneSoft[tone]\n            )}\n          >\n            AK\n          </span>\n          <div className=\"min-w-0 flex-1 pr-6\">\n            <div className=\"font-medium tracking-tight text-foreground\">Ada Kern</div>\n            <div className=\"mt-0.5 truncate text-muted-foreground\">\n              Left a comment on your pull request.\n            </div>\n          </div>\n          <CornerClose onClick={() => dismiss(t.id)} />\n        </>\n      )}\n    </RichShell>\n  )\n}\n\n/* ---------------------------------------------------------------- MediaToaster\n   Kare medya kucuk resmi + baslik + dosya bilgisi. */\nexport function MediaToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: RichToasterProps) {\n  return (\n    <RichShell className={className} size={size} tone={tone} label={label}>\n      {(t, dismiss) => (\n        <>\n          <span\n            aria-hidden\n            className=\"inline-flex size-10 shrink-0 items-center justify-center rounded-lg border border-border bg-muted text-muted-foreground\"\n          >\n            <ImageIcon />\n          </span>\n          <div className=\"min-w-0 flex-1 pr-6\">\n            <div className=\"truncate font-medium tracking-tight text-foreground\">\n              cover-art.png\n            </div>\n            <div className=\"mt-0.5 text-muted-foreground\">1.4 MB uploaded</div>\n          </div>\n          <CornerClose onClick={() => dismiss(t.id)} />\n        </>\n      )}\n    </RichShell>\n  )\n}\n\n/* ---------------------------------------------------- DescriptionToaster\n   Baslik + uzun aciklama + meta satiri + aksiyon butonu. */\nexport function DescriptionToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: RichToasterProps) {\n  const Icon = toneIcon[tone]\n  return (\n    <RichShell\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      bodyClass=\"flex-col items-stretch\"\n    >\n      {(t, dismiss) => (\n        <>\n          <CornerClose onClick={() => dismiss(t.id)} />\n          <div className=\"flex items-start gap-3 pr-6\">\n            <Icon className={cn(\"mt-0.5\", toneIconColor[tone])} />\n            <div className=\"min-w-0 flex-1\">\n              <div className=\"font-medium tracking-tight text-foreground\">Backup finished</div>\n              <p className=\"mt-1 text-muted-foreground\">\n                All 248 files were copied to the archive bucket. Older snapshots are pruned\n                automatically after thirty days.\n              </p>\n              <div className=\"mt-2 text-xs text-muted-foreground\">Just now - archive-eu-1</div>\n            </div>\n          </div>\n          <div className=\"mt-3 flex justify-end\">\n            <button\n              type=\"button\"\n              className=\"inline-flex h-8 select-none items-center justify-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground outline-none transition-colors hover:bg-[color-mix(in_oklab,var(--color-primary)_88%,var(--color-foreground))] focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n              onClick={() => dismiss(t.id)}\n            >\n              View log\n            </button>\n          </div>\n        </>\n      )}\n    </RichShell>\n  )\n}\n\n/* -------------------------------------------------------------- StackedToaster\n   Gruplanmis bildirim: sayac rozeti + kisa liste. */\nconst stackedRows = [\"Ada Kern approved the design\", \"Bruno Salt requested changes\"]\n\nexport function StackedToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: RichToasterProps) {\n  return (\n    <RichShell\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      bodyClass=\"flex-col items-stretch\"\n    >\n      {(t, dismiss) => (\n        <>\n          <CornerClose onClick={() => dismiss(t.id)} />\n          <div className=\"flex items-center gap-2 pr-6\">\n            <span\n              aria-hidden\n              className={cn(\n                \"inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-xs font-medium tabular-nums\",\n                toneSoft[tone]\n              )}\n            >\n              2\n            </span>\n            <div className=\"font-medium tracking-tight text-foreground\">New review activity</div>\n          </div>\n          <ul className=\"mt-2 space-y-1\">\n            {stackedRows.map((row) => (\n              <li\n                key={row}\n                className=\"flex items-center gap-2 rounded-md bg-[color-mix(in_oklab,var(--color-foreground)_4%,transparent)] px-2 py-1 text-muted-foreground\"\n              >\n                <span\n                  aria-hidden\n                  className={cn(\"inline-block size-1.5 shrink-0 rounded-full\", toneIconColor[tone])}\n                  style={{ backgroundColor: \"currentColor\" }}\n                />\n                <span className=\"min-w-0 truncate\">{row}</span>\n              </li>\n            ))}\n          </ul>\n        </>\n      )}\n    </RichShell>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/sonner-rich.tsx"
    }
  ],
  "categories": [
    "styled",
    "sonner"
  ],
  "type": "registry:component"
}