{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notification",
  "title": "Notification",
  "description": "Positionable toast with status, avatar, and countdown.",
  "dependencies": [
    "lucide-react@^1.33.0"
  ],
  "registryDependencies": [
    "https://components.exe.xyz/r/avatar.json",
    "https://components.exe.xyz/r/button.json",
    "https://components.exe.xyz/r/close-button.json",
    "https://components.exe.xyz/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/notification.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CircleAlertIcon, CircleCheckIcon, InfoIcon } from \"lucide-react\"\nimport type { LucideIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport { CloseButton } from \"@/components/ui/close-button\"\n\ntype NotificationStatus = \"information\" | \"success\" | \"error\"\ntype NotificationPosition =\n  | \"top-left\"\n  | \"top-center\"\n  | \"top-right\"\n  | \"bottom-left\"\n  | \"bottom-center\"\n  | \"bottom-right\"\n\nconst statusIcon: Record<NotificationStatus, LucideIcon> = {\n  information: InfoIcon,\n  success: CircleCheckIcon,\n  error: CircleAlertIcon,\n}\n\nconst statusAccent: Record<NotificationStatus, string> = {\n  information: \"border-l-foreground/40\",\n  success: \"border-l-[var(--chart-2)]\",\n  error: \"border-l-destructive\",\n}\n\nconst statusIconClass: Record<NotificationStatus, string> = {\n  information: \"text-foreground\",\n  success: \"text-[var(--chart-2)]\",\n  error: \"text-destructive\",\n}\n\nconst presenceClass = {\n  online: \"bg-[var(--chart-2)]\",\n  busy: \"bg-primary\",\n  offline: \"bg-muted-foreground\",\n} as const\n\nconst positionClass: Record<NotificationPosition, string> = {\n  \"top-left\": \"items-start justify-start\",\n  \"top-center\": \"items-start justify-center\",\n  \"top-right\": \"items-start justify-end\",\n  \"bottom-left\": \"items-end justify-start\",\n  \"bottom-center\": \"items-end justify-center\",\n  \"bottom-right\": \"items-end justify-end\",\n}\n\nfunction NotificationViewport({\n  position = \"top-right\",\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  position?: NotificationPosition\n}) {\n  const fromBottom = position.startsWith(\"bottom\")\n\n  return (\n    <div\n      data-slot=\"notification-viewport\"\n      data-position={position}\n      className={cn(\n        \"pointer-events-none inset-0 z-50 flex p-4\",\n        className?.match(/(^|\\s)(absolute|relative|sticky)\\b/)\n          ? \"absolute\"\n          : \"fixed\",\n        positionClass[position],\n        className\n      )}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"pointer-events-auto flex w-full max-w-sm flex-col gap-2\",\n          fromBottom && \"flex-col-reverse\"\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  )\n}\n\nfunction useAutoDismiss(\n  duration: number | undefined,\n  paused: boolean,\n  onDismiss: (() => void) | undefined\n) {\n  const [progress, setProgress] = React.useState(1)\n  const remaining = React.useRef(duration ?? 0)\n  const last = React.useRef<number | null>(null)\n  const onDismissRef = React.useRef(onDismiss)\n  onDismissRef.current = onDismiss\n\n  React.useEffect(() => {\n    remaining.current = duration ?? 0\n    setProgress(1)\n    last.current = null\n  }, [duration])\n\n  React.useEffect(() => {\n    if (duration == null || duration <= 0) return\n    if (paused) {\n      last.current = null\n      return\n    }\n    let raf = 0\n    const tick = (now: number) => {\n      if (last.current == null) last.current = now\n      remaining.current = Math.max(0, remaining.current - (now - last.current))\n      last.current = now\n      setProgress(remaining.current / duration)\n      if (remaining.current <= 0) {\n        onDismissRef.current?.()\n        return\n      }\n      raf = requestAnimationFrame(tick)\n    }\n    raf = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(raf)\n  }, [duration, paused])\n\n  return progress\n}\n\nfunction Notification({\n  status = \"information\",\n  title,\n  description,\n  timestamp,\n  icon: IconProp,\n  avatar,\n  actions,\n  autoDismissDuration,\n  introDelay = 0,\n  onDismiss,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  status?: NotificationStatus\n  title: React.ReactNode\n  description?: React.ReactNode\n  timestamp?: React.ReactNode\n  icon?: LucideIcon\n  avatar?: {\n    src?: string\n    alt?: string\n    initials?: string\n    presence?: \"online\" | \"busy\" | \"offline\"\n  }\n  actions?: {\n    label: string\n    variant?: \"primary\" | \"secondary\" | \"danger\"\n    onClick: () => void\n  }[]\n  autoDismissDuration?: number\n  introDelay?: number\n  onDismiss?: () => void\n}) {\n  const [ready, setReady] = React.useState(introDelay <= 0)\n  const [paused, setPaused] = React.useState(false)\n  const Icon = IconProp ?? statusIcon[status]\n  const progress = useAutoDismiss(\n    ready ? autoDismissDuration : undefined,\n    paused,\n    onDismiss\n  )\n\n  React.useEffect(() => {\n    if (introDelay <= 0) return\n    const id = window.setTimeout(() => setReady(true), introDelay)\n    return () => window.clearTimeout(id)\n  }, [introDelay])\n\n  if (!ready) return null\n\n  const buttonVariant = {\n    primary: \"default\",\n    secondary: \"outline\",\n    danger: \"destructive\",\n  } as const\n\n  return (\n    <div\n      role={status === \"error\" ? \"alert\" : \"status\"}\n      data-slot=\"notification\"\n      data-status={status}\n      onMouseEnter={() => setPaused(true)}\n      onMouseLeave={() => setPaused(false)}\n      onFocusCapture={() => setPaused(true)}\n      onBlurCapture={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget)) {\n          setPaused(false)\n        }\n      }}\n      className={cn(\n        \"relative flex w-full gap-3 overflow-hidden rounded-[2px] border border-l-2 border-border bg-card p-3 text-sm shadow-sm\",\n        \"animate-in duration-150 fade-in-0 slide-in-from-top-2 motion-reduce:animate-none motion-reduce:slide-in-from-top-0\",\n        statusAccent[status],\n        className\n      )}\n      {...props}\n    >\n      {avatar ? (\n        <span className=\"relative mt-0.5 shrink-0\">\n          <Avatar size=\"sm\">\n            {avatar.src ? (\n              <AvatarImage src={avatar.src} alt={avatar.alt} />\n            ) : null}\n            <AvatarFallback>{avatar.initials}</AvatarFallback>\n          </Avatar>\n          {avatar.presence ? (\n            <span\n              data-slot=\"notification-presence\"\n              className={cn(\n                \"absolute right-0 bottom-0 size-2 rounded-full ring-2 ring-card\",\n                presenceClass[avatar.presence]\n              )}\n            />\n          ) : null}\n        </span>\n      ) : (\n        <Icon\n          className={cn(\"mt-0.5 size-4 shrink-0\", statusIconClass[status])}\n        />\n      )}\n      <div className=\"min-w-0 flex-1\">\n        <div className=\"flex items-start gap-2\">\n          <p className=\"min-w-0 flex-1 font-medium\">{title}</p>\n          {onDismiss ? (\n            <CloseButton\n              size=\"sm\"\n              aria-label=\"Dismiss notification\"\n              onClick={onDismiss}\n              className=\"-mt-0.5 -mr-1\"\n            />\n          ) : null}\n        </div>\n        {description ? (\n          <p className=\"mt-0.5 text-muted-foreground\">{description}</p>\n        ) : null}\n        {timestamp ? (\n          <p className=\"mt-1 font-mono text-[11px] text-muted-foreground\">\n            {timestamp}\n          </p>\n        ) : null}\n        {actions && actions.length > 0 ? (\n          <div className=\"mt-2 flex flex-wrap gap-1.5\">\n            {actions.map((action) => (\n              <Button\n                key={action.label}\n                size=\"xs\"\n                variant={buttonVariant[action.variant ?? \"secondary\"]}\n                onClick={action.onClick}\n              >\n                {action.label}\n              </Button>\n            ))}\n          </div>\n        ) : null}\n      </div>\n      {autoDismissDuration ? (\n        <span\n          aria-hidden\n          data-slot=\"notification-countdown\"\n          className=\"absolute inset-x-0 bottom-0 h-[3px] origin-left bg-primary\"\n          style={{ transform: `scaleX(${progress})` }}\n        />\n      ) : null}\n    </div>\n  )\n}\n\nexport { Notification, NotificationViewport }\nexport type { NotificationPosition, NotificationStatus }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}