{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sonner-progress",
  "title": "Progress sonner",
  "description": "Styled sonner: the Progress set. 5 decorative sonner variations (BarToaster, RingToaster, CountdownToaster, SweepToaster, EdgeToaster) 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-progress.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 - progress 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: an indicator showing the time left\n   until auto-dismiss (a bottom bar, a ring, a countdown, a sweep, an edge line). The\n   duration is a FIXED constant (TOAST_MS) - the indicator is locked to it, making it\n   deterministic (NO Date.now/Math.random).\n   Under reduced-motion the indicator animation is skipped (it stays static) but the\n   toast still auto-dismisses. Color comes ONLY from tokens, via alpha color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nexport type StyledTone = \"success\" | \"info\" | \"warning\" | \"danger\"\n\n/* Toast genisligi size'a bagli. */\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 -> gosterge dolgu rengi (semantic token). */\nconst toneFill: Record<StyledTone, string> = {\n  success: \"bg-success\",\n  info: \"bg-info\",\n  warning: \"bg-warning\",\n  danger: \"bg-danger\",\n}\n\n/* Tone -> sweep katmani (token uzerinden color-mix alpha). */\nconst toneSweep: Record<StyledTone, string> = {\n  success: \"bg-[color-mix(in_oklab,var(--color-success)_14%,transparent)]\",\n  info: \"bg-[color-mix(in_oklab,var(--color-info)_14%,transparent)]\",\n  warning: \"bg-[color-mix(in_oklab,var(--color-warning)_14%,transparent)]\",\n  danger: \"bg-[color-mix(in_oklab,var(--color-danger)_14%,transparent)]\",\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). The indicator is locked to exactly this\n   duration. */\nconst TOAST_MS = 5000\nconst TOAST_SEC = TOAST_MS / 1000\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  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 ProgressToasterProps {\n  className?: string\n  size?: StyledSize\n  tone?: StyledTone\n  label?: React.ReactNode\n}\n\n/* Toast body: every variant is identical apart from the indicator. The indicator and overlay slots carry the variant-specific visual. */\nfunction ProgressShell({\n  message,\n  className,\n  size,\n  tone,\n  label,\n  indicator,\n  overlay,\n  leading,\n}: Required<Pick<ProgressToasterProps, \"size\" | \"tone\">> & {\n  message: string\n  className?: string\n  label?: React.ReactNode\n  /* The indicator added below or beside the toast. */\n  indicator?: React.ReactNode\n  /* Toast yuzeyini kaplayan gosterge (sweep). */\n  overlay?: React.ReactNode\n  /* The indicator standing in for the icon (a ring/countdown). */\n  leading?: React.ReactNode\n}) {\n  const reduce = useReducedMotion()\n  const { items, push, dismiss } = useToastStack(message)\n  const m = toastMotion(reduce)\n  const Icon = toneIcon[tone]\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])}\n              initial={m.initial}\n              animate={m.animate}\n              exit={m.exit}\n              transition={m.transition}\n              layout={!reduce}\n            >\n              {overlay}\n              {leading ?? <Icon className={cn(\"relative mt-0.5\", toneIconColor[tone])} />}\n              <div className=\"relative min-w-0 flex-1 pt-0.5\">{t.message}</div>\n              <button\n                type=\"button\"\n                aria-label=\"Dismiss\"\n                className={cn(closeBtn, \"relative\")}\n                onClick={() => dismiss(t.id)}\n              >\n                <X />\n              </button>\n              {indicator}\n            </motion.div>\n          ))}\n        </AnimatePresence>\n      </div>\n    </div>\n  )\n}\n\n/* ------------------------------------------------------------------ BarToaster A progress bar shrinking beneath the toast. */\nexport function BarToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: ProgressToasterProps) {\n  const reduce = useReducedMotion()\n  return (\n    <ProgressShell\n      message=\"Saving your draft...\"\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      indicator={\n        <div\n          aria-hidden\n          className=\"absolute inset-x-0 bottom-0 h-1 bg-[color-mix(in_oklab,var(--color-foreground)_8%,transparent)]\"\n        >\n          {reduce ? (\n            <div className={cn(\"h-full w-full\", toneFill[tone])} />\n          ) : (\n            <motion.div\n              className={cn(\"h-full\", toneFill[tone])}\n              initial={{ width: \"100%\" }}\n              animate={{ width: \"0%\" }}\n              transition={{ duration: TOAST_SEC, ease: \"linear\" }}\n            />\n          )}\n        </div>\n      }\n    />\n  )\n}\n\n/* ----------------------------------------------------------------- RingToaster\n   Ikon yerinde bosalan dairesel halka. */\nexport function RingToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: ProgressToasterProps) {\n  const reduce = useReducedMotion()\n  return (\n    <ProgressShell\n      message=\"Syncing your workspace...\"\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      leading={\n        <span\n          aria-hidden\n          className={cn(\"relative mt-0.5 inline-flex shrink-0\", toneIconColor[tone])}\n        >\n          <svg viewBox=\"0 0 20 20\" fill=\"none\" className=\"size-4 -rotate-90\">\n            <circle\n              cx=\"10\"\n              cy=\"10\"\n              r=\"8\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2.5\"\n              strokeOpacity=\"0.2\"\n            />\n            {reduce ? (\n              <circle\n                cx=\"10\"\n                cy=\"10\"\n                r=\"8\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2.5\"\n                strokeLinecap=\"round\"\n              />\n            ) : (\n              <motion.circle\n                cx=\"10\"\n                cy=\"10\"\n                r=\"8\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2.5\"\n                strokeLinecap=\"round\"\n                initial={{ pathLength: 1 }}\n                animate={{ pathLength: 0 }}\n                transition={{ duration: TOAST_SEC, ease: \"linear\" }}\n              />\n            )}\n          </svg>\n        </span>\n      }\n    />\n  )\n}\n\n/* A small badge counting the remaining seconds. It mounts with every toast and the interval is cleared on unmount. Under reduced-motion a static starting value is shown. */\nfunction Countdown({ tone, reduce }: { tone: StyledTone; reduce: boolean | null }) {\n  const [left, setLeft] = React.useState(Math.round(TOAST_SEC))\n\n  React.useEffect(() => {\n    if (reduce) return\n    const interval = setInterval(() => {\n      setLeft((prev) => (prev > 0 ? prev - 1 : 0))\n    }, 1000)\n    return () => clearInterval(interval)\n  }, [reduce])\n\n  return (\n    <span\n      aria-hidden\n      className={cn(\n        \"relative mt-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded-full border border-current text-[10px] font-medium leading-none tabular-nums\",\n        toneIconColor[tone]\n      )}\n    >\n      {left}\n    </span>\n  )\n}\n\n/* ------------------------------------------------------------ CountdownToaster\n   Kalan saniyeyi rakamla sayan rozet. */\nexport function CountdownToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: ProgressToasterProps) {\n  const reduce = useReducedMotion()\n  return (\n    <ProgressShell\n      message=\"Closing this notice shortly.\"\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      leading={<Countdown tone={tone} reduce={reduce} />}\n    />\n  )\n}\n\n/* ---------------------------------------------------------------- SweepToaster\n   Toast yuzeyini kaplayan ton katmani soldan saga cekilir. */\nexport function SweepToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: ProgressToasterProps) {\n  const reduce = useReducedMotion()\n  return (\n    <ProgressShell\n      message=\"Queued for delivery.\"\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      overlay={\n        reduce ? (\n          <div aria-hidden className={cn(\"absolute inset-0\", toneSweep[tone])} />\n        ) : (\n          <motion.div\n            aria-hidden\n            className={cn(\"absolute inset-0 origin-left\", toneSweep[tone])}\n            initial={{ scaleX: 1 }}\n            animate={{ scaleX: 0 }}\n            transition={{ duration: TOAST_SEC, ease: \"linear\" }}\n          />\n        )\n      }\n    />\n  )\n}\n\n/* ----------------------------------------------------------------- EdgeToaster A vertical line on the left edge draining from top to bottom. */\nexport function EdgeToaster({\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: ProgressToasterProps) {\n  const reduce = useReducedMotion()\n  return (\n    <ProgressShell\n      message=\"Report is being generated.\"\n      className={className}\n      size={size}\n      tone={tone}\n      label={label}\n      indicator={\n        <div\n          aria-hidden\n          className=\"absolute inset-y-0 left-0 w-1 bg-[color-mix(in_oklab,var(--color-foreground)_8%,transparent)]\"\n        >\n          {reduce ? (\n            <div className={cn(\"h-full w-full\", toneFill[tone])} />\n          ) : (\n            <motion.div\n              className={cn(\"h-full w-full origin-top\", toneFill[tone])}\n              initial={{ scaleY: 1 }}\n              animate={{ scaleY: 0 }}\n              transition={{ duration: TOAST_SEC, ease: \"linear\" }}\n            />\n          )}\n        </div>\n      }\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/sonner-progress.tsx"
    }
  ],
  "categories": [
    "styled",
    "sonner"
  ],
  "type": "registry:component"
}