{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sonner-compact",
  "title": "Compact sonner",
  "description": "Styled sonner: the Compact set. 5 decorative sonner variations (MinimalToaster, InlineToaster, PillToaster, IconToaster, BareToaster) 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-compact.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AlertTriangle, CheckCircle, Info, X } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Styled sonner - compact family: 5 self-contained toasters. NO sonner package and\n   NO 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: a minimal/dense body - narrow\n   padding, a single line, a pill form, icon-weighted, or without a frame/shadow.\n   The small close buttons (under 24px) carry an invisible hit-area extension\n   (after:-inset-2). Color comes ONLY from tokens, via alpha color-mix.\n   Deterministic: the ids come from a ref counter (NO Date.now/Math.random). */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nexport type StyledTone = \"success\" | \"info\" | \"warning\" | \"danger\"\n\n/* Toast width follows size (for fixed-width bodies). */\nconst toastWidth: Record<StyledSize, string> = {\n  sm: \"w-64\",\n  md: \"w-72\",\n  lg: \"w-80\",\n  xl: \"w-96\",\n}\n\n/* Yogun govdenin padding + gap olcegi. */\nconst compactPad: Record<StyledSize, string> = {\n  sm: \"gap-2 px-2.5 py-1.5\",\n  md: \"gap-2 px-3 py-2\",\n  lg: \"gap-2.5 px-3.5 py-2.5\",\n  xl: \"gap-3 px-4 py-3\",\n}\n\n/* Yogun govdenin yazi olcegi. */\nconst compactText: Record<StyledSize, string> = {\n  sm: \"text-xs\",\n  md: \"text-sm\",\n  lg: \"text-sm\",\n  xl: \"text-base\",\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\n/* A close button that stays under 24px: the visual box does not grow, the touch area does (after:-inset-2 -> a 32px target). The icon measurements are written with important: the [&_svg]:size-4 rule on the body has the same specificity and would otherwise override them. */\nconst tinyCloseBtn =\n  \"relative inline-flex size-4 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none transition-colors after:absolute after:-inset-2 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-3! [&_svg]:shrink-0 [&_i]:text-xs! [&_i]:leading-none\"\n\nconst compactBase =\n  \"pointer-events-auto relative flex items-center overflow-hidden [&_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/* Otomatik kapanma suresi (sabit, deterministik). */\nconst TOAST_MS = 4000\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: 24, scale: 0.96 },\n    animate: { opacity: 1, x: 0, scale: 1 },\n    exit: { opacity: 0, x: 24, scale: 0.96 },\n    transition: { type: \"spring\" as const, stiffness: 340, damping: 28 },\n  }\n}\n\ninterface ToastItem {\n  id: number\n  message: string\n}\n\n/* Shared stack logic: ref-counted id, fixed timeout, every timer cleared on\n   unmount (no setState-after-unmount). */\nfunction useToastStack(message: string) {\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, message }])\n    const timer = setTimeout(() => dismiss(id), TOAST_MS)\n    timers.current.set(id, timer)\n  }, [dismiss, message])\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 CompactToasterProps {\n  className?: string\n  size?: StyledSize\n  tone?: StyledTone\n  label?: React.ReactNode\n}\n\n/* Single body: bodyClass plus the content rendering are variant-specific. */\nfunction CompactShell({\n  message,\n  className,\n  size,\n  tone,\n  label,\n  bodyClass,\n  children,\n}: Required<Pick<CompactToasterProps, \"size\" | \"tone\">> & {\n  message: string\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(message)\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(compactBase, compactPad[size], compactText[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/* -------------------------------------------------------------- MinimalToaster A one-line message with no icon and tight padding plus a small close. */\nexport function MinimalToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: CompactToasterProps) {\n  return (\n    <CompactShell\n      message=\"Draft saved.\"\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      bodyClass={cn(\n        \"rounded-lg border border-border bg-popover text-popover-foreground shadow-sm\",\n        toastWidth[size]\n      )}\n    >\n      {(t, dismiss) => (\n        <>\n          <div className=\"min-w-0 flex-1 truncate\">{t.message}</div>\n          <button\n            type=\"button\"\n            aria-label=\"Dismiss\"\n            className={tinyCloseBtn}\n            onClick={() => dismiss(t.id)}\n          >\n            <X />\n          </button>\n        </>\n      )}\n    </CompactShell>\n  )\n}\n\n/* --------------------------------------------------------------- InlineToaster Icon plus message on one line, width following the content; no close button, it closes on timeout. */\nexport function InlineToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: CompactToasterProps) {\n  const Icon = toneIcon[tone]\n  return (\n    <CompactShell\n      message=\"Copied to clipboard\"\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      bodyClass=\"max-w-full rounded-lg border border-border bg-popover text-popover-foreground shadow-sm\"\n    >\n      {(t) => (\n        <>\n          <Icon className={toneIconColor[tone]} />\n          <span className=\"min-w-0 truncate\">{t.message}</span>\n        </>\n      )}\n    </CompactShell>\n  )\n}\n\n/* -----------------------------------------------------------------\n   PillToaster\n   A fully rounded pill form, over a tone surface. */\nexport function PillToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: CompactToasterProps) {\n  const Icon = toneIcon[tone]\n  return (\n    <CompactShell\n      message=\"Link shared\"\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      bodyClass={cn(\n        \"max-w-full rounded-full border border-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)] shadow-sm\",\n        toneSoft[tone]\n      )}\n    >\n      {(t, dismiss) => (\n        <>\n          <Icon />\n          <span className=\"min-w-0 truncate font-medium\">{t.message}</span>\n          <button\n            type=\"button\"\n            aria-label=\"Dismiss\"\n            className={tinyCloseBtn}\n            onClick={() => dismiss(t.id)}\n          >\n            <X />\n          </button>\n        </>\n      )}\n    </CompactShell>\n  )\n}\n\n/* ----------------------------------------------------------------- IconToaster\n   Ikon agirlikli: renkli ton karesi + kisa etiket. */\nexport function IconToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: CompactToasterProps) {\n  const Icon = toneIcon[tone]\n  return (\n    <CompactShell\n      message=\"Uploaded\"\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      bodyClass=\"max-w-full rounded-lg border border-border bg-popover text-popover-foreground shadow-sm\"\n    >\n      {(t) => (\n        <>\n          <span\n            className={cn(\n              \"inline-flex size-6 shrink-0 items-center justify-center rounded-md\",\n              toneSoft[tone]\n            )}\n          >\n            <Icon />\n          </span>\n          <span className=\"min-w-0 truncate font-medium\">{t.message}</span>\n        </>\n      )}\n    </CompactShell>\n  )\n}\n\n/* -----------------------------------------------------------------\n   BareToaster\n   No frame and no shadow: just text on a soft surface. */\nexport function BareToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: CompactToasterProps) {\n  return (\n    <CompactShell\n      message=\"Nothing to see here.\"\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      bodyClass={cn(\n        \"rounded-md bg-[color-mix(in_oklab,var(--color-foreground)_88%,transparent)] text-background\",\n        toastWidth[size]\n      )}\n    >\n      {(t, dismiss) => (\n        <>\n          <div className=\"min-w-0 flex-1 truncate\">{t.message}</div>\n          <button\n            type=\"button\"\n            aria-label=\"Dismiss\"\n            className={cn(tinyCloseBtn, \"text-background/70 hover:text-background\")}\n            onClick={() => dismiss(t.id)}\n          >\n            <X />\n          </button>\n        </>\n      )}\n    </CompactShell>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/sonner-compact.tsx"
    }
  ],
  "categories": [
    "styled",
    "sonner"
  ],
  "type": "registry:component"
}