{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "toast", "type": "registry:ui", "title": "Toast", "description": "Provider + hook based transient notifications.", "registryDependencies": [ "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/utils.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/text.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/theme.json" ], "dependencies": [ "lucide-react-native" ], "files": [ { "path": "registry/shared/ui/toast.tsx", "type": "registry:ui", "target": "components/ui/toast.tsx", "content": "import { CircleCheck, CircleX, Info, TriangleAlert } from 'lucide-react-native';\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { ActivityIndicator, Animated, Pressable, View } from 'react-native';\nimport { Text } from '@/components/ui/text';\nimport { cn, useIconColor } from '@/lib/utils';\n\nexport type ToastType =\n | 'default'\n | 'success'\n | 'error'\n | 'info'\n | 'warning'\n | 'loading';\n\nexport interface ToastButtonData {\n label: string;\n onPress?: () => void;\n}\n\nexport interface ToastOptions {\n id?: number;\n description?: string;\n /** Mirrors shadcn legacy variant — 'destructive' renders a filled error toast. */\n variant?: 'default' | 'destructive';\n /** Right-side action button, mirrors Sonner `action`. */\n action?: ToastButtonData;\n /** Right-side dismiss button, mirrors Sonner `cancel`. */\n cancel?: ToastButtonData;\n /** Icon override — set to null to hide. */\n icon?: ReactNode | null;\n /** Auto-dismiss delay in ms. Default 4000; Infinity keeps it visible. */\n duration?: number;\n}\n\nexport interface ToastData extends ToastOptions {\n id: number;\n title: string;\n type: ToastType;\n}\n\ninterface ToastApi {\n (title: string, opts?: ToastOptions): number;\n (data: Omit & { type?: ToastType }): number;\n success: (title: string, opts?: ToastOptions) => number;\n error: (title: string, opts?: ToastOptions) => number;\n info: (title: string, opts?: ToastOptions) => number;\n warning: (title: string, opts?: ToastOptions) => number;\n message: (title: string, opts?: ToastOptions) => number;\n /** Persistent until dismissed or updated via the same `id`. */\n loading: (title: string, opts?: ToastOptions) => number;\n promise: (\n promise: Promise,\n msgs: {\n loading: string;\n success: string | ((value: T) => string);\n error: string | ((err: unknown) => string);\n },\n opts?: ToastOptions\n ) => number;\n dismiss: (id?: number) => void;\n}\n\nconst ToastContext = createContext<{ toast: ToastApi }>({\n toast: (() => 0) as unknown as ToastApi,\n});\n\nexport const useToast = () => useContext(ToastContext);\n\nlet nextId = 0;\n\nexport function ToastProvider({\n children,\n /** Where toasts stack, mirrors Sonner `position`. */\n position = 'bottom',\n /** Colored type accents on the toast itself, mirrors Sonner `richColors`. */\n richColors = false,\n}: {\n children: ReactNode;\n position?: 'top' | 'bottom';\n richColors?: boolean;\n}) {\n const [toasts, setToasts] = useState([]);\n const timers = useRef(new Map>());\n\n const dismiss = useCallback((id?: number) => {\n setToasts((prev) =>\n id === undefined ? [] : prev.filter((x) => x.id !== id)\n );\n }, []);\n\n const show = useCallback(\n (t: Omit & { id?: number }) => {\n const id = t.id ?? ++nextId;\n const toastData = { ...t, id };\n setToasts((prev) =>\n prev.some((x) => x.id === id)\n ? prev.map((x) => (x.id === id ? toastData : x))\n : [...prev, toastData]\n );\n const timer = timers.current.get(id);\n if (timer) clearTimeout(timer);\n const duration = t.duration ?? (t.type === 'loading' ? Infinity : 4000);\n if (duration !== Infinity) {\n timers.current.set(\n id,\n setTimeout(() => dismiss(id), duration)\n );\n }\n return id;\n },\n [dismiss]\n );\n\n const toast = useMemo(() => {\n const api = ((\n titleOrData: string | Omit,\n opts?: ToastOptions\n ) =>\n show(\n typeof titleOrData === 'string'\n ? { ...opts, title: titleOrData, type: 'default' }\n : { type: 'default', ...titleOrData }\n )) as ToastApi;\n for (const type of [\n 'success',\n 'error',\n 'info',\n 'warning',\n 'message',\n ] as const) {\n api[type] = (title, opts) =>\n show({ ...opts, title, type: type === 'message' ? 'default' : type });\n }\n api.loading = (title, opts) =>\n show({ duration: Infinity, ...opts, title, type: 'loading' });\n api.promise = (promise, msgs, opts) => {\n const id = show({ type: 'loading', title: msgs.loading, ...opts });\n promise\n .then((v) =>\n show({\n ...opts,\n id,\n type: 'success',\n title:\n typeof msgs.success === 'function'\n ? msgs.success(v)\n : msgs.success,\n })\n )\n .catch((e) =>\n show({\n ...opts,\n id,\n type: 'error',\n title:\n typeof msgs.error === 'function' ? msgs.error(e) : msgs.error,\n })\n );\n return id;\n };\n api.dismiss = dismiss;\n return api;\n }, [show, dismiss]);\n\n return (\n \n {children}\n \n {toasts.map((t) => (\n dismiss(t.id)}\n />\n ))}\n \n \n );\n}\n\nconst TYPE_COLORS = {\n success: '#16a34a',\n error: '#ef4444',\n info: '#0ea5e9',\n warning: '#f59e0b',\n} as const;\n\nfunction ToastIcon({ type, filled }: { type: ToastType; filled: boolean }) {\n const foreground = useIconColor('foreground');\n const size = 16;\n if (type === 'loading') {\n return ;\n }\n if (type === 'default') return null;\n const color = filled ? '#fafafa' : TYPE_COLORS[type];\n const Icon = {\n success: CircleCheck,\n error: CircleX,\n info: Info,\n warning: TriangleAlert,\n }[type];\n return ;\n}\n\nfunction Toast({\n data,\n richColors,\n onDismiss,\n}: {\n data: ToastData;\n richColors: boolean;\n onDismiss: () => void;\n}) {\n const opacity = useRef(new Animated.Value(0)).current;\n useEffect(() => {\n Animated.timing(opacity, {\n toValue: 1,\n duration: 150,\n useNativeDriver: true,\n }).start();\n }, [opacity]);\n const dismiss = () => {\n Animated.timing(opacity, {\n toValue: 0,\n duration: 120,\n useNativeDriver: true,\n }).start(onDismiss);\n };\n\n // Legacy `variant=\"destructive\"` and richColors typed toasts fill the color.\n const tint =\n data.type in TYPE_COLORS\n ? TYPE_COLORS[data.type as keyof typeof TYPE_COLORS]\n : undefined;\n const filled = data.variant === 'destructive' || (richColors && !!tint);\n\n return (\n \n \n \n {data.icon === undefined ? (\n \n ) : (\n data.icon\n )}\n \n \n {data.title}\n \n {!!data.description && (\n \n {data.description}\n \n )}\n \n \n {!!data.action && (\n {\n data.action?.onPress?.();\n dismiss();\n }}\n className={cn(\n 'rounded-md border px-3 py-1.5',\n filled ? 'border-destructive-foreground/40' : 'border-border'\n )}\n >\n \n {data.action.label}\n \n \n )}\n {!!data.cancel && (\n {\n data.cancel?.onPress?.();\n dismiss();\n }}\n className=\"rounded-md px-3 py-1.5\"\n >\n \n {data.cancel.label}\n \n \n )}\n \n \n );\n}\n" } ] }