{"version":3,"file":"use-slow-loading-DI3acknC.mjs","names":[],"sources":["../src/util/use-slow-loading.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\n/**\n * How long something must be loading before an indicator appears. Loads that finish inside this\n * window never render one at all.\n */\nexport const LOADING_DELAY_MS = 300;\n\n/**\n * Once an indicator is on screen, how long it stays there even if the wait has already ended.\n * Applies only to waits that already crossed {@link LOADING_DELAY_MS} — its whole job is to stop a\n * just-shown indicator vanishing a frame later.\n */\nexport const LOADING_MIN_DURATION_MS = 300;\n\nexport interface SlowLoadingOptions {\n  /** Don't surface before the wait has lasted this long. Defaults to {@link LOADING_DELAY_MS}. */\n  after?: number;\n  /**\n   * Once surfaced, stay up at least this long. Defaults to {@link LOADING_MIN_DURATION_MS}.\n   */\n  minDuration?: number;\n}\n\n/**\n * Whether a wait has gone on long enough to be worth telling the user about.\n *\n * Loading UI flashes on fast responses: a request that resolves in 60 ms produces a 60 ms skeleton —\n * long enough to see, too short to read, and it happens on every navigation. This is the standard\n * two-part fix, in one place:\n *\n * - **A threshold.** A wait shorter than `after` never surfaces at all.\n * - **A floor.** Once it has surfaced, it stays for `minDuration`.\n *\n * Both halves are needed. A threshold alone turns a 320 ms wait into a 20 ms flash, which is worse\n * than either extreme.\n *\n * **This returns a third state, and a caller that renders two will be wrong.** The point of the\n * threshold is that there is a window where the wait is real but not yet worth mentioning — so\n * \"not slow\" does not mean \"ready\", and the data may still be missing:\n *\n * ```tsx\n * const slow = useSlowLoading(!list.loaded);\n *\n * if (slow) return <Skeleton />;\n * if (!list.loaded) return null; // loading, but too early to say so\n * return <Content rows={list.value} />;\n * ```\n *\n * Both branch orders matter:\n *\n * - `slow` is tested **first** because the floor outlives the wait. Once surfaced, this stays true\n *   for `minDuration` even after the value has landed, and testing the value first would swap the\n *   content in immediately — which is the flash the floor exists to prevent.\n * - The `null` branch is what makes the threshold real. Drop it and the first `after` milliseconds\n *   render the content branch with nothing to put in it.\n *\n * | value    | `slow` | render                              |\n * | -------- | ------ | ----------------------------------- |\n * | missing  | false  | nothing — inside the threshold      |\n * | missing  | true   | the skeleton                        |\n * | present  | true   | the skeleton, held by the floor     |\n * | present  | false  | the content                         |\n *\n * `LazyObserver` and `<Table.Loading>` are this same sequence, already wired up; reach for the hook\n * where a component renders its own skeleton.\n *\n * Plain boolean in, plain boolean out, so it works the same inside an `observer()` and outside one —\n * pass it anything, including a value read from a lazy or a store.\n *\n * `after: 0` surfaces immediately and `minDuration: 0` hides immediately, which together collapse\n * the third state away: with both at zero, `slow` and the wait are the same flag and two branches\n * are enough.\n */\nexport function useSlowLoading(active: boolean, options?: SlowLoadingOptions): boolean {\n  const after = options?.after ?? LOADING_DELAY_MS;\n  const minDuration = options?.minDuration ?? LOADING_MIN_DURATION_MS;\n\n  const [shown, setShown] = useState(false);\n  const shownAt = useRef(0);\n\n  useEffect(() => {\n    if (active) {\n      // Already up: the floor below owns when it comes down, so there is nothing to schedule.\n      if (shown) return;\n\n      if (after <= 0) {\n        shownAt.current = Date.now();\n        setShown(true);\n        return;\n      }\n\n      const timer = setTimeout(() => {\n        shownAt.current = Date.now();\n        setShown(true);\n      }, after);\n      // Cleanup on a falling edge cancels the timer, so a wait that ends inside the threshold\n      // never surfaces — and a flicker restarts the window rather than accumulating toward it.\n      return () => clearTimeout(timer);\n    }\n\n    // Never surfaced, so there is nothing to hold on screen.\n    if (!shown) return;\n\n    const remaining = shownAt.current + minDuration - Date.now();\n    if (remaining <= 0) {\n      setShown(false);\n      return;\n    }\n\n    const timer = setTimeout(() => setShown(false), remaining);\n    // If the wait resumes before this fires, the cleanup cancels the hide and the indicator stays\n    // up continuously rather than blinking off and on.\n    return () => clearTimeout(timer);\n  }, [active, shown, after, minDuration]);\n\n  return shown;\n}\n"],"mappings":";;;;;;;AAMA,MAAa,mBAAmB;;;;;;AAOhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DvC,SAAgB,eAAe,QAAiB,SAAuC;CACrF,MAAM,QAAQ,SAAS;CACvB,MAAM,cAAc,SAAS;CAE7B,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,MAAM,UAAU,OAAO,CAAC;CAExB,gBAAgB;EACd,IAAI,QAAQ;GAEV,IAAI,OAAO;GAEX,IAAI,SAAS,GAAG;IACd,QAAQ,UAAU,KAAK,IAAI;IAC3B,SAAS,IAAI;IACb;GACF;GAEA,MAAM,QAAQ,iBAAiB;IAC7B,QAAQ,UAAU,KAAK,IAAI;IAC3B,SAAS,IAAI;GACf,GAAG,KAAK;GAGR,aAAa,aAAa,KAAK;EACjC;EAGA,IAAI,CAAC,OAAO;EAEZ,MAAM,YAAY,QAAQ,UAAU,cAAc,KAAK,IAAI;EAC3D,IAAI,aAAa,GAAG;GAClB,SAAS,KAAK;GACd;EACF;EAEA,MAAM,QAAQ,iBAAiB,SAAS,KAAK,GAAG,SAAS;EAGzD,aAAa,aAAa,KAAK;CACjC,GAAG;EAAC;EAAQ;EAAO;EAAO;CAAW,CAAC;CAEtC,OAAO;AACT"}