{"name":"toast-gooey","type":"registry:ui","files":[{"path":"toast-gooey.tsx","type":"registry:ui","content":"import {\n\ttype CSSProperties,\n\ttype MouseEventHandler,\n\ttype ReactNode,\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport { Gooey } from \"./toast-gooey-renderer\";\nimport \"./toast-gooey.css\";\nimport {\n\tGOOEY_POSITIONS,\n\ttype GooeyOptions,\n\ttype GooeyPosition,\n\ttype GooeyState,\n} from \"./toast-gooey-types\";\nexport type { GooeyOptions, GooeyState } from \"./toast-gooey-types\";\n\n/* -------------------------------- Constants ------------------------------- */\n\nconst DEFAULT_DURATION = 6000;\nconst EXIT_DURATION = DEFAULT_DURATION * 0.1;\nconst AUTO_EXPAND_DELAY = DEFAULT_DURATION * 0.025;\nconst AUTO_COLLAPSE_DELAY = DEFAULT_DURATION - 2000;\n\nconst pillAlign = (pos: GooeyPosition) =>\n\tpos.includes(\"right\") ? \"right\" : pos.includes(\"center\") ? \"center\" : \"left\";\nconst expandDir = (pos: GooeyPosition) =>\n\tpos.startsWith(\"top\") ? (\"bottom\" as const) : (\"top\" as const);\n\n/* ---------------------------------- Types --------------------------------- */\n\ninterface InternalGooeyOptions extends GooeyOptions {\n\tid?: string;\n\tstate?: GooeyState;\n}\n\ninterface GooeyItem extends InternalGooeyOptions {\n\tid: string;\n\tinstanceId: string;\n\texiting?: boolean;\n\tautoExpandDelayMs?: number;\n\tautoCollapseDelayMs?: number;\n}\n\ntype GooeyOffsetValue = number | string;\ntype GooeyOffsetConfig = Partial<\n\tRecord<\"top\" | \"right\" | \"bottom\" | \"left\", GooeyOffsetValue>\n>;\n\nexport interface GooeyToasterProps {\n\tchildren?: ReactNode;\n\tposition?: GooeyPosition;\n\toffset?: GooeyOffsetValue | GooeyOffsetConfig;\n\toptions?: Partial<GooeyOptions>;\n}\n\n/* ------------------------------ Global State ------------------------------ */\n\ntype GooeyListener = (toasts: GooeyItem[]) => void;\n\nconst store = {\n\ttoasts: [] as GooeyItem[],\n\tlisteners: new Set<GooeyListener>(),\n\tposition: \"top-right\" as GooeyPosition,\n\toptions: undefined as Partial<GooeyOptions> | undefined,\n\n\temit() {\n\t\tfor (const fn of this.listeners) fn(this.toasts);\n\t},\n\n\tupdate(fn: (prev: GooeyItem[]) => GooeyItem[]) {\n\t\tthis.toasts = fn(this.toasts);\n\t\tthis.emit();\n\t},\n};\n\nlet idCounter = 0;\nconst generateId = () =>\n\t`${++idCounter}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n\nconst timeoutKey = (t: GooeyItem) => `${t.id}:${t.instanceId}`;\n\n/* ------------------------------- Toast API -------------------------------- */\n\nconst dismissToast = (id: string) => {\n\tconst item = store.toasts.find((t) => t.id === id);\n\tif (!item || item.exiting) return;\n\n\tstore.update((prev) =>\n\t\tprev.map((t) => (t.id === id ? { ...t, exiting: true } : t)),\n\t);\n\n\tsetTimeout(\n\t\t() => store.update((prev) => prev.filter((t) => t.id !== id)),\n\t\tEXIT_DURATION,\n\t);\n};\n\nconst resolveAutopilot = (\n\topts: InternalGooeyOptions,\n\tduration: number | null,\n): { expandDelayMs?: number; collapseDelayMs?: number } => {\n\tif (opts.autopilot === false || !duration || duration <= 0) return {};\n\tconst cfg = typeof opts.autopilot === \"object\" ? opts.autopilot : undefined;\n\tconst clamp = (v: number) => Math.min(duration, Math.max(0, v));\n\treturn {\n\t\texpandDelayMs: clamp(cfg?.expand ?? AUTO_EXPAND_DELAY),\n\t\tcollapseDelayMs: clamp(cfg?.collapse ?? AUTO_COLLAPSE_DELAY),\n\t};\n};\n\nconst mergeOptions = (options: InternalGooeyOptions) => ({\n\t...store.options,\n\t...options,\n\tstyles: { ...store.options?.styles, ...options.styles },\n});\n\nconst buildGooeyItem = (\n\tmerged: InternalGooeyOptions,\n\tid: string,\n\tfallbackPosition?: GooeyPosition,\n): GooeyItem => {\n\tconst duration = merged.duration ?? DEFAULT_DURATION;\n\tconst auto = resolveAutopilot(merged, duration);\n\treturn {\n\t\t...merged,\n\t\tid,\n\t\tinstanceId: generateId(),\n\t\tposition: merged.position ?? fallbackPosition ?? store.position,\n\t\tautoExpandDelayMs: auto.expandDelayMs,\n\t\tautoCollapseDelayMs: auto.collapseDelayMs,\n\t};\n};\n\nconst createToast = (options: InternalGooeyOptions) => {\n\tconst live = store.toasts.filter((t) => !t.exiting);\n\tconst merged = mergeOptions(options);\n\n\tconst id = merged.id ?? \"gooey-default\";\n\tconst prev = live.find((t) => t.id === id);\n\tconst item = buildGooeyItem(merged, id, prev?.position);\n\n\tif (prev) {\n\t\tstore.update((p) => p.map((t) => (t.id === id ? item : t)));\n\t} else {\n\t\tstore.update((p) => [...p.filter((t) => t.id !== id), item]);\n\t}\n\treturn { id, duration: merged.duration ?? DEFAULT_DURATION };\n};\n\nconst updateToast = (id: string, options: InternalGooeyOptions) => {\n\tconst existing = store.toasts.find((t) => t.id === id);\n\tif (!existing) return;\n\n\tconst item = buildGooeyItem(mergeOptions(options), id, existing.position);\n\tstore.update((prev) => prev.map((t) => (t.id === id ? item : t)));\n};\n\nexport interface GooeyPromiseOptions<T = unknown> {\n\tloading: Pick<GooeyOptions, \"title\" | \"icon\">;\n\tsuccess: GooeyOptions | ((data: T) => GooeyOptions);\n\terror: GooeyOptions | ((err: unknown) => GooeyOptions);\n\taction?: GooeyOptions | ((data: T) => GooeyOptions);\n\tposition?: GooeyPosition;\n}\n\nexport const gooey = {\n\tshow: (opts: GooeyOptions) => createToast(opts).id,\n\tsuccess: (opts: GooeyOptions) =>\n\t\tcreateToast({ ...opts, state: \"success\" }).id,\n\terror: (opts: GooeyOptions) => createToast({ ...opts, state: \"error\" }).id,\n\twarning: (opts: GooeyOptions) =>\n\t\tcreateToast({ ...opts, state: \"warning\" }).id,\n\tinfo: (opts: GooeyOptions) => createToast({ ...opts, state: \"info\" }).id,\n\taction: (opts: GooeyOptions) => createToast({ ...opts, state: \"action\" }).id,\n\n\tpromise: <T,>(\n\t\tpromise: Promise<T> | (() => Promise<T>),\n\t\topts: GooeyPromiseOptions<T>,\n\t): Promise<T> => {\n\t\tconst { id } = createToast({\n\t\t\t...opts.loading,\n\t\t\tstate: \"loading\",\n\t\t\tduration: null,\n\t\t\tposition: opts.position,\n\t\t});\n\n\t\tconst p = typeof promise === \"function\" ? promise() : promise;\n\n\t\tp.then((data) => {\n\t\t\tif (opts.action) {\n\t\t\t\tconst actionOpts =\n\t\t\t\t\ttypeof opts.action === \"function\" ? opts.action(data) : opts.action;\n\t\t\t\tupdateToast(id, { ...actionOpts, state: \"action\", id });\n\t\t\t} else {\n\t\t\t\tconst successOpts =\n\t\t\t\t\ttypeof opts.success === \"function\"\n\t\t\t\t\t\t? opts.success(data)\n\t\t\t\t\t\t: opts.success;\n\t\t\t\tupdateToast(id, { ...successOpts, state: \"success\", id });\n\t\t\t}\n\t\t}).catch((err) => {\n\t\t\tconst errorOpts =\n\t\t\t\ttypeof opts.error === \"function\" ? opts.error(err) : opts.error;\n\t\t\tupdateToast(id, { ...errorOpts, state: \"error\", id });\n\t\t});\n\n\t\treturn p;\n\t},\n\n\tdismiss: dismissToast,\n\n\tclear: (position?: GooeyPosition) =>\n\t\tstore.update((prev) =>\n\t\t\tposition ? prev.filter((t) => t.position !== position) : [],\n\t\t),\n};\n\n/* ------------------------------ Toaster Component ------------------------- */\n\nexport function Toaster({\n\tchildren,\n\tposition = \"top-right\",\n\toffset,\n\toptions,\n}: GooeyToasterProps) {\n\tconst [toasts, setToasts] = useState<GooeyItem[]>(store.toasts);\n\tconst [hoveredId, setHoveredId] = useState<string>();\n\n\tconst hoverRef = useRef(false);\n\tconst timersRef = useRef(new Map<string, number>());\n\tconst listRef = useRef(toasts);\n\n\tuseEffect(() => {\n\t\tstore.position = position;\n\t\tstore.options = options;\n\t}, [position, options]);\n\n\tconst clearAllTimers = useCallback(() => {\n\t\tfor (const t of timersRef.current.values()) clearTimeout(t);\n\t\ttimersRef.current.clear();\n\t}, []);\n\n\tconst schedule = useCallback((items: GooeyItem[]) => {\n\t\tif (hoverRef.current) return;\n\n\t\tfor (const item of items) {\n\t\t\tif (item.exiting) continue;\n\t\t\tconst key = timeoutKey(item);\n\t\t\tif (timersRef.current.has(key)) continue;\n\n\t\t\tconst dur = item.duration ?? DEFAULT_DURATION;\n\t\t\tif (dur === null || dur <= 0) continue;\n\n\t\t\ttimersRef.current.set(\n\t\t\t\tkey,\n\t\t\t\twindow.setTimeout(() => dismissToast(item.id), dur),\n\t\t\t);\n\t\t}\n\t}, []);\n\n\tuseEffect(() => {\n\t\tconst listener: GooeyListener = (next) => setToasts(next);\n\t\tstore.listeners.add(listener);\n\t\treturn () => {\n\t\t\tstore.listeners.delete(listener);\n\t\t\tclearAllTimers();\n\t\t};\n\t}, [clearAllTimers]);\n\n\tuseEffect(() => {\n\t\tlistRef.current = toasts;\n\n\t\tconst toastKeys = new Set(toasts.map(timeoutKey));\n\t\tfor (const [key, timer] of timersRef.current) {\n\t\t\tif (!toastKeys.has(key)) {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\ttimersRef.current.delete(key);\n\t\t\t}\n\t\t}\n\t\tschedule(toasts);\n\t}, [toasts, schedule]);\n\n\tconst handleMouseEnter = useCallback<\n\t\tMouseEventHandler<HTMLButtonElement>\n\t>(() => {\n\t\tif (hoverRef.current) return;\n\t\thoverRef.current = true;\n\t\tclearAllTimers();\n\t}, [clearAllTimers]);\n\n\tconst handleMouseLeave = useCallback<\n\t\tMouseEventHandler<HTMLButtonElement>\n\t>(() => {\n\t\tif (!hoverRef.current) return;\n\t\thoverRef.current = false;\n\t\tschedule(listRef.current);\n\t}, [schedule]);\n\n\tconst latest = useMemo(() => {\n\t\tfor (let i = toasts.length - 1; i >= 0; i--) {\n\t\t\tif (!toasts[i].exiting) return toasts[i].id;\n\t\t}\n\t\treturn undefined;\n\t}, [toasts]);\n\n\tconst activeId = hoveredId ?? latest;\n\n\tconst getHandlers = useCallback(\n\t\t(toastId: string) => ({\n\t\t\tenter: ((e) => {\n\t\t\t\tsetHoveredId(toastId);\n\t\t\t\thandleMouseEnter(e);\n\t\t\t}) as MouseEventHandler<HTMLButtonElement>,\n\t\t\tleave: ((e) => {\n\t\t\t\tsetHoveredId(undefined);\n\t\t\t\thandleMouseLeave(e);\n\t\t\t}) as MouseEventHandler<HTMLButtonElement>,\n\t\t\tdismiss: () => dismissToast(toastId),\n\t\t}),\n\t\t[handleMouseEnter, handleMouseLeave],\n\t);\n\n\tconst getViewportStyle = useCallback(\n\t\t(pos: GooeyPosition): CSSProperties | undefined => {\n\t\t\tif (offset === undefined) return undefined;\n\n\t\t\tconst o =\n\t\t\t\ttypeof offset === \"object\"\n\t\t\t\t\t? offset\n\t\t\t\t\t: { top: offset, right: offset, bottom: offset, left: offset };\n\n\t\t\tconst s: CSSProperties = {};\n\t\t\tconst px = (v: GooeyOffsetValue) =>\n\t\t\t\ttypeof v === \"number\" ? `${v}px` : v;\n\n\t\t\tif (pos.startsWith(\"top\") && o.top) s.top = px(o.top);\n\t\t\tif (pos.startsWith(\"bottom\") && o.bottom) s.bottom = px(o.bottom);\n\t\t\tif (pos.endsWith(\"left\") && o.left) s.left = px(o.left);\n\t\t\tif (pos.endsWith(\"right\") && o.right) s.right = px(o.right);\n\n\t\t\treturn s;\n\t\t},\n\t\t[offset],\n\t);\n\n\tconst byPosition = useMemo(() => {\n\t\tconst map = {} as Partial<Record<GooeyPosition, GooeyItem[]>>;\n\t\tfor (const t of toasts) {\n\t\t\tconst pos = t.position ?? position;\n\t\t\tconst arr = map[pos];\n\t\t\tif (arr) {\n\t\t\t\tarr.push(t);\n\t\t\t} else {\n\t\t\t\tmap[pos] = [t];\n\t\t\t}\n\t\t}\n\t\treturn map;\n\t}, [toasts, position]);\n\n\treturn (\n\t\t<>\n\t\t\t{children}\n\t\t\t{GOOEY_POSITIONS.map((pos) => {\n\t\t\t\tconst items = byPosition[pos];\n\t\t\t\tif (!items?.length) return null;\n\n\t\t\t\tconst pill = pillAlign(pos);\n\t\t\t\tconst expand = expandDir(pos);\n\n\t\t\t\treturn (\n\t\t\t\t\t<section\n\t\t\t\t\t\tkey={pos}\n\t\t\t\t\t\tdata-gooey-viewport\n\t\t\t\t\t\tdata-position={pos}\n\t\t\t\t\t\taria-live=\"polite\"\n\t\t\t\t\t\tstyle={getViewportStyle(pos)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{items.map((item) => {\n\t\t\t\t\t\t\tconst h = getHandlers(item.id);\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Gooey\n\t\t\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\t\t\tid={item.id}\n\t\t\t\t\t\t\t\t\tstate={item.state}\n\t\t\t\t\t\t\t\t\ttitle={item.title}\n\t\t\t\t\t\t\t\t\tdescription={item.description}\n\t\t\t\t\t\t\t\t\tposition={pill}\n\t\t\t\t\t\t\t\t\texpand={expand}\n\t\t\t\t\t\t\t\t\ticon={item.icon}\n\t\t\t\t\t\t\t\t\tfill={item.fill}\n\t\t\t\t\t\t\t\t\tstyles={item.styles}\n\t\t\t\t\t\t\t\t\tbutton={item.button}\n\t\t\t\t\t\t\t\t\troundness={item.roundness}\n\t\t\t\t\t\t\t\t\texiting={item.exiting}\n\t\t\t\t\t\t\t\t\tautoExpandDelayMs={item.autoExpandDelayMs}\n\t\t\t\t\t\t\t\t\tautoCollapseDelayMs={item.autoCollapseDelayMs}\n\t\t\t\t\t\t\t\t\trefreshKey={item.instanceId}\n\t\t\t\t\t\t\t\t\tcanExpand={activeId === undefined || activeId === item.id}\n\t\t\t\t\t\t\t\t\tonMouseEnter={h.enter}\n\t\t\t\t\t\t\t\t\tonMouseLeave={h.leave}\n\t\t\t\t\t\t\t\t\tonDismiss={h.dismiss}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</section>\n\t\t\t\t);\n\t\t\t})}\n\t\t</>\n\t);\n}\n","target":"components/ui/toast-gooey.tsx"}],"registryDependencies":["https://www.honestui.com/r/toast-gooey-renderer.json","https://www.honestui.com/r/toast-gooey-types.json","https://www.honestui.com/r/toast-gooey.css.json"]}