{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-nav-reset",
  "type": "registry:hook",
  "title": "useRegisterNavReset",
  "description": "Double-tap the nav item for the page you are already on to return to the top, then to the page's start.",
  "categories": [
    "hooks",
    "navigation"
  ],
  "files": [
    {
      "path": "hooks/use-nav-reset.ts",
      "type": "registry:hook",
      "target": "hooks/use-nav-reset.ts",
      "content": "import * as React from \"react\";\n\n/**\n * Double-tapping the nav item for the page you are already on takes you back to\n * the start of that page, in two steps: first the top of the scroll region,\n * then -- if the page has somewhere further back to go -- whatever it registered\n * as its start.\n *\n * The step is chosen from scroll position rather than from a second gesture\n * inside a time window, so the sequence is self-correcting: a double-tap when\n * scrolled always scrolls, and one at the top always resets.\n */\n\n/** Milliseconds within which a second activation counts as a double. */\nexport const NAV_DOUBLE_MS = 350;\n\n/** Scroll offsets below this count as \"at the top\" (sub-pixel, momentum drift). */\nconst AT_TOP_EPSILON = 4;\n\n/**\n * A page publishes its start here; the nav consumes it. The two share no\n * parent -- the nav is Layout chrome and the tabs belong to the page -- so the\n * slot is a ref rather than state: it is read at click time and never needs to\n * re-render the nav.\n */\nconst NavResetContext = React.createContext<React.MutableRefObject<(() => void) | null> | null>(\n  null,\n);\n\nexport interface NavResetProviderProps {\n  children: React.ReactNode;\n}\n\n/**\n * Installs the slot. `AppShell` renders this, so an app built on the shell has\n * it already. Without a provider `useRegisterNavReset` is inert and the nav\n * stops after the scroll step, which is what a page with no tabs wants anyway.\n */\nexport function NavResetProvider({ children }: NavResetProviderProps) {\n  const slot = React.useRef<(() => void) | null>(null);\n  return React.createElement(NavResetContext.Provider, { value: slot }, children);\n}\n\n/**\n * Registers what \"back to the start\" means for the page that calls it -- for a\n * tabbed page, selecting the first tab.\n *\n * ```tsx\n * useRegisterNavReset(() => setActiveTab(tabItems[0].id));\n * ```\n *\n * Pass `null` while the page has nowhere to go back to (already on the first\n * tab, or no tabs at all) and the nav's second step does nothing. The callback\n * is read through a ref, so an inline arrow does not re-register on every\n * render and the nav always invokes the latest one.\n */\nexport function useRegisterNavReset(fn: (() => void) | null): void {\n  const slot = React.useContext(NavResetContext);\n  const latest = React.useRef(fn);\n  latest.current = fn;\n\n  React.useEffect(() => {\n    if (!slot) return;\n    // `has` is what the nav tests to decide whether a second step exists, so it\n    // has to track the CURRENT value rather than the one at registration time.\n    const call = () => latest.current?.();\n    const entry = latest.current ? call : null;\n    slot.current = entry;\n    return () => {\n      if (slot.current === entry) slot.current = null;\n    };\n  });\n}\n\n/** The registered reset, or null. Consumed by the nav components. */\nexport function useNavReset(): React.MutableRefObject<(() => void) | null> | null {\n  return React.useContext(NavResetContext);\n}\n\n/**\n * Resolves the scroll region the same way `useScrollReset` does: an element\n * with overflow of its own is the scroller, otherwise the window is. A Layout\n * hands its `AppMain` ref to both and each figures out the scroll model.\n */\ninterface ScrollHandle {\n  atTop: boolean;\n  get: () => number;\n  set: (top: number) => void;\n  /** Where user input lands while the region scrolls -- a real wheel/touch/key\n   * cancels the animation, the user's gesture wins. */\n  events: EventTarget;\n}\n\nfunction scroller(el: HTMLElement | null): ScrollHandle {\n  if (el && el.scrollHeight > el.clientHeight) {\n    return {\n      atTop: el.scrollTop <= AT_TOP_EPSILON,\n      get: () => el.scrollTop,\n      set: (top) => {\n        el.scrollTop = top;\n      },\n      events: el,\n    };\n  }\n  return {\n    atTop: window.scrollY <= AT_TOP_EPSILON,\n    get: () => window.scrollY,\n    set: (top) => window.scrollTo(0, top),\n    events: window,\n  };\n}\n\n// The double-tap's scroll-to-top animates along a settle curve evaluated in\n// JS -- scrollTop has no CSS transition to inherit from, so the values live\n// here rather than in the tokens. The curve winds up from rest (ease-in),\n// accelerates through the travel, and eases into the top: the arrival IS the\n// stop, no rebound.\nconst SCROLL_MS = 300; // duration-300\nconst SETTLE = cubicBezier(0.45, 0, 0.2, 1);\n\n/** Standard CSS cubic-bezier evaluation: solve the x polynomial for t\n * (Newton, bisection fallback), then read y. */\nfunction cubicBezier(x1: number, y1: number, x2: number, y2: number): (x: number) => number {\n  const cx = 3 * x1;\n  const bx = 3 * (x2 - x1) - cx;\n  const ax = 1 - cx - bx;\n  const cy = 3 * y1;\n  const by = 3 * (y2 - y1) - cy;\n  const ay = 1 - cy - by;\n  const sampleX = (t: number) => ((ax * t + bx) * t + cx) * t;\n  const sampleY = (t: number) => ((ay * t + by) * t + cy) * t;\n  return (x: number) => {\n    if (x <= 0) return 0;\n    if (x >= 1) return 1;\n    let t = x;\n    for (let i = 0; i < 8; i++) {\n      const err = sampleX(t) - x;\n      if (Math.abs(err) < 1e-5) return sampleY(t);\n      const d = (3 * ax * t + 2 * bx) * t + cx;\n      if (Math.abs(d) < 1e-6) break;\n      t -= err / d;\n    }\n    let lo = 0;\n    let hi = 1;\n    while (hi - lo > 1e-5) {\n      t = (lo + hi) / 2;\n      if (sampleX(t) < x) lo = t;\n      else hi = t;\n    }\n    return sampleY(t);\n  };\n}\n\nconst CANCEL_EVENTS = [\"wheel\", \"touchstart\", \"pointerdown\", \"keydown\"] as const;\n// One animation per region: a fresh double-tap restarts cleanly instead of\n// two frame loops fighting over the same scrollTop.\nconst running = new WeakMap<EventTarget, () => void>();\n\nfunction animateToTop(region: ScrollHandle): void {\n  const start = region.get();\n  if (start <= 0) return;\n  if (\n    typeof matchMedia !== \"undefined\" &&\n    matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  ) {\n    region.set(0);\n    return;\n  }\n\n  running.get(region.events)?.();\n  let raf = 0;\n  let done = false;\n  const cancel = () => {\n    done = true;\n    cancelAnimationFrame(raf);\n    for (const type of CANCEL_EVENTS) region.events.removeEventListener(type, cancel);\n    running.delete(region.events);\n  };\n  for (const type of CANCEL_EVENTS) region.events.addEventListener(type, cancel, { passive: true });\n  running.set(region.events, cancel);\n\n  let t0: number | null = null;\n  const frame = (now: number) => {\n    if (done) return;\n    if (t0 === null) t0 = now;\n    if (now - t0 >= SCROLL_MS) {\n      region.set(0);\n      cancel();\n      return;\n    }\n    region.set(start * (1 - SETTLE((now - t0) / SCROLL_MS)));\n    raf = requestAnimationFrame(frame);\n  };\n  raf = requestAnimationFrame(frame);\n}\n\n/**\n * Returns the handler a nav item fires on activation. It only acts on the\n * SECOND activation within `NAV_DOUBLE_MS`, and only for the item whose page is\n * already showing -- activating any other item is a navigation, which owns the\n * gesture.\n */\nexport function useNavDoubleActivate(\n  scrollRef?: React.RefObject<HTMLElement | null>,\n): (active: boolean) => void {\n  const resetSlot = useNavReset();\n  const lastAt = React.useRef(0);\n\n  return React.useCallback(\n    (active: boolean) => {\n      if (!active) {\n        lastAt.current = 0;\n        return;\n      }\n      const now = Date.now();\n      if (now - lastAt.current > NAV_DOUBLE_MS) {\n        lastAt.current = now;\n        return;\n      }\n      // Consume the pair, so a third tap starts a new one rather than\n      // triggering again off the second.\n      lastAt.current = 0;\n\n      const region = scroller(scrollRef?.current ?? null);\n      if (!region.atTop) {\n        animateToTop(region);\n        return;\n      }\n      resetSlot?.current?.();\n    },\n    [resetSlot, scrollRef],\n  );\n}\n"
    }
  ],
  "docs": "Two steps, chosen by scroll position rather than by a second gesture in a time window, so the sequence is self-correcting: a double-tap while scrolled goes to the top, and one already at the top runs whatever the page registered as its start. Pass the AppMain ref as scrollRef to HeaderNav and MobileBottomNav – they are Layout chrome rendered alongside AppMain, so they sit above the scroll region it publishes and cannot read it, the same reason useScrollReset takes a ref. In a tabbed page call useRegisterNavReset(() => setActiveTab(first)), and pass null when there is nowhere further back to go so the second step stays inert. AppShell installs the provider; without one the gesture stops after the scroll step, which is what a page with no tabs wants.",
  "meta": {
    "group": "hooks",
    "related": [
      "app-shell",
      "mobile-bottom-nav",
      "use-scroll-region"
    ],
    "exports": [
      "useRegisterNavReset",
      "NavResetProvider",
      "NAV_DOUBLE_MS"
    ],
    "siteSlug": "use-nav-reset"
  }
}
