{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sonner-motion",
  "title": "Motion sonner",
  "description": "Styled sonner: the Motion set. 5 decorative sonner variations (SlideToaster, PopToaster, FadeToaster, FlipToaster, BounceToaster) 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-motion.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 - motion 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: the character of the enter/exit motion\n   (slide, pop, fade, flip, bounce). AnimatePresence drives it; under reduced-motion\n   there is NO transform, only opacity.\n   Color comes ONLY from tokens, via alpha color-mix. Deterministic: the ids come\n   from a ref 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. */\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\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/* Otomatik kapanma suresi (sabit, deterministik). */\nconst TOAST_MS = 4000\n\ntype Flavor = \"slide\" | \"pop\" | \"fade\" | \"flip\" | \"bounce\"\n\n/* Under reduced-motion every flavour is the same: no transform, a short opacity transition. */\nconst reducedMotionSpec = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: { opacity: 0 },\n  transition: { duration: 0.12 },\n}\n\n/* Flavour -> enter/exit spec. */\nfunction flavorMotion(flavor: Flavor, reduce: boolean | null) {\n  if (reduce) return reducedMotionSpec\n  switch (flavor) {\n    case \"slide\":\n      return {\n        initial: { opacity: 0, x: 40 },\n        animate: { opacity: 1, x: 0 },\n        exit: { opacity: 0, x: 40 },\n        transition: { type: \"spring\" as const, stiffness: 320, damping: 30 },\n      }\n    case \"pop\":\n      return {\n        initial: { opacity: 0, scale: 0.8 },\n        animate: { opacity: 1, scale: 1 },\n        exit: { opacity: 0, scale: 0.8 },\n        transition: { type: \"spring\" as const, stiffness: 520, damping: 24 },\n      }\n    case \"fade\":\n      return {\n        initial: { opacity: 0, y: 8 },\n        animate: { opacity: 1, y: 0 },\n        exit: { opacity: 0, y: 8 },\n        transition: { duration: 0.28, ease: \"easeOut\" as const },\n      }\n    case \"flip\":\n      return {\n        initial: { opacity: 0, rotateX: -70, y: 12 },\n        animate: { opacity: 1, rotateX: 0, y: 0 },\n        exit: { opacity: 0, rotateX: 55, y: 12 },\n        transition: { duration: 0.34, ease: \"easeOut\" as const },\n      }\n    case \"bounce\":\n    default:\n      return {\n        initial: { opacity: 0, y: 48, scale: 0.94 },\n        animate: { opacity: 1, y: 0, scale: 1 },\n        exit: { opacity: 0, y: 24, scale: 0.94 },\n        transition: { type: \"spring\" as const, stiffness: 480, damping: 12, mass: 0.9 },\n      }\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 MotionToasterProps {\n  className?: string\n  size?: StyledSize\n  tone?: StyledTone\n  label?: React.ReactNode\n}\n\n/* A single body: only the motion flavor and the message change. */\nfunction MotionToaster({\n  flavor,\n  message,\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: MotionToasterProps & { flavor: Flavor; message: string }) {\n  const reduce = useReducedMotion()\n  const { items, push, dismiss } = useToastStack(message)\n  const m = flavorMotion(flavor, 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} style={reduce ? undefined : { perspective: 800 }}>\n        <AnimatePresence initial={false}>\n          {items.map((t) => (\n            <motion.div\n              key={t.id}\n              role=\"status\"\n              data-tone={tone}\n              data-motion={flavor}\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              <Icon className={cn(\"mt-0.5\", toneIconColor[tone])} />\n              <div className=\"min-w-0 flex-1 pt-0.5\">{t.message}</div>\n              <button\n                type=\"button\"\n                aria-label=\"Dismiss\"\n                className={closeBtn}\n                onClick={() => dismiss(t.id)}\n              >\n                <X />\n              </button>\n            </motion.div>\n          ))}\n        </AnimatePresence>\n      </div>\n    </div>\n  )\n}\n\n/* ---------------------------------------------------------------- SlideToaster\n   Kenardan yaylanarak iceri kayar. */\nexport function SlideToaster(props: MotionToasterProps) {\n  return <MotionToaster flavor=\"slide\" message=\"Slid in from the edge.\" {...props} />\n}\n\n/* ------------------------------------------------------------------\n   PopToaster\n   It pops open by growing from small. */\nexport function PopToaster(props: MotionToasterProps) {\n  return <MotionToaster flavor=\"pop\" message=\"Popped into place.\" {...props} />\n}\n\n/* ----------------------------------------------------------------- FadeToaster\n   Kisa bir yukselisle yumusakca belirir. */\nexport function FadeToaster(props: MotionToasterProps) {\n  return <MotionToaster flavor=\"fade\" message=\"Faded in quietly.\" {...props} />\n}\n\n/* -----------------------------------------------------------------\n   FlipToaster\n   It opens by rotating on the X axis (with perspective). */\nexport function FlipToaster(props: MotionToasterProps) {\n  return <MotionToaster flavor=\"flip\" message=\"Flipped in on its edge.\" {...props} />\n}\n\n/* --------------------------------------------------------------- BounceToaster\n   Dusuk sonumlu yay ile zipliyarak oturur. */\nexport function BounceToaster(props: MotionToasterProps) {\n  return <MotionToaster flavor=\"bounce\" message=\"Bounced up from below.\" {...props} />\n}\n",
      "type": "registry:component",
      "target": "components/ui/sonner-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "sonner"
  ],
  "type": "registry:component"
}