{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sonner-position",
  "title": "Position sonner",
  "description": "Styled sonner: the Position set. 5 decorative sonner variations (TopRightToaster, TopCenterToaster, BottomLeftToaster, BottomCenterToaster, TopLeftToaster) 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-position.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 - position 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). The difference: where the stack anchors on\n   screen (top-right, top-center, bottom-left, bottom-center, top-left). The entry\n   direction is chosen from the anchor. Multiple toasts stack on top of each other\n   and disappear on a timeout or via the close button. Color comes ONLY from tokens,\n   via alpha color-mix. Deterministic: the ids come from a ref counter (NO\n   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\n/* Otomatik kapanma suresi (sabit, deterministik). */\nconst TOAST_MS = 4000\n\ntype Anchor = \"top-right\" | \"top-center\" | \"bottom-left\" | \"bottom-center\" | \"top-left\"\n\n/* Ankraj -> fixed konum + hizalama. */\nconst anchorClass: Record<Anchor, string> = {\n  \"top-right\": \"top-0 right-0 items-end\",\n  \"top-center\": \"top-0 left-1/2 -translate-x-1/2 items-center\",\n  \"bottom-left\": \"bottom-0 left-0 items-start\",\n  \"bottom-center\": \"bottom-0 left-1/2 -translate-x-1/2 items-center\",\n  \"top-left\": \"top-0 left-0 items-start\",\n}\n\n/* Ankraj -> giris/cikis ofseti. Kenardan iceri kayar. */\nconst anchorOffset: Record<Anchor, { x: number; y: number }> = {\n  \"top-right\": { x: 32, y: 0 },\n  \"top-center\": { x: 0, y: -24 },\n  \"bottom-left\": { x: -32, y: 0 },\n  \"bottom-center\": { x: 0, y: 24 },\n  \"top-left\": { x: -32, y: 0 },\n}\n\nconst stackClass = \"pointer-events-none fixed z-50 flex flex-col gap-2 p-4\"\n\n/* A slide+fade relative to the anchor; instant under reduced-motion (opacity\n   only). */\nfunction toastMotion(anchor: Anchor, 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  const off = anchorOffset[anchor]\n  return {\n    initial: { opacity: 0, x: off.x, y: off.y, scale: 0.96 },\n    animate: { opacity: 1, x: 0, y: 0, scale: 1 },\n    exit: { opacity: 0, x: off.x, y: off.y, 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 PositionToasterProps {\n  className?: string\n  size?: StyledSize\n  tone?: StyledTone\n  label?: React.ReactNode\n}\n\n/* A single body: only the anchor and the message change. */\nfunction PositionToaster({\n  anchor,\n  message,\n  className,\n  size = \"md\",\n  tone = \"info\",\n  label = \"Show toast\",\n}: PositionToasterProps & { anchor: Anchor; message: string }) {\n  const reduce = useReducedMotion()\n  const { items, push, dismiss } = useToastStack(message)\n  const m = toastMotion(anchor, 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={cn(stackClass, anchorClass[anchor])}>\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              <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/* ------------------------------------------------------------- TopRightToaster\n   Stack sag ust kosede toplanir, sagdan iceri kayar. */\nexport function TopRightToaster(props: PositionToasterProps) {\n  return <PositionToaster anchor=\"top-right\" message=\"Pinned to the top right.\" {...props} />\n}\n\n/* ------------------------------------------------------------ TopCenterToaster\n   Stack ust orta hatta toplanir, yukaridan asagi iner. */\nexport function TopCenterToaster(props: PositionToasterProps) {\n  return <PositionToaster anchor=\"top-center\" message=\"Dropped in from the top.\" {...props} />\n}\n\n/* ----------------------------------------------------------- BottomLeftToaster\n   Stack sol alt kosede toplanir, soldan iceri kayar. */\nexport function BottomLeftToaster(props: PositionToasterProps) {\n  return <PositionToaster anchor=\"bottom-left\" message=\"Anchored bottom left.\" {...props} />\n}\n\n/* --------------------------------------------------------- BottomCenterToaster\n   Stack alt orta hatta toplanir, asagidan yukari kalkar. */\nexport function BottomCenterToaster(props: PositionToasterProps) {\n  return <PositionToaster anchor=\"bottom-center\" message=\"Raised from the bottom.\" {...props} />\n}\n\n/* -------------------------------------------------------------- TopLeftToaster\n   Stack sol ust kosede toplanir, soldan iceri kayar. */\nexport function TopLeftToaster(props: PositionToasterProps) {\n  return <PositionToaster anchor=\"top-left\" message=\"Parked at the top left.\" {...props} />\n}\n",
      "type": "registry:component",
      "target": "components/ui/sonner-position.tsx"
    }
  ],
  "categories": [
    "styled",
    "sonner"
  ],
  "type": "registry:component"
}