# `@lazarv/react-server/navigation`

Client navigation surface: `<Link>`, `<Form>`, `<Refresh>`, `<ReactServerComponent>`, hooks for location/search params/matching, navigation guards, redirect helpers, and scroll restoration.

## Functions

### `createRoute`

```ts
function createRoute<TPath extends string, TParams = ExtractParams<TPath>, TSearch = Record<string, string>>(path: TPath, options?: ClientRouteOptions<TParams, TSearch>): RouteDescriptor<TPath, TParams, TSearch>;
function createRoute(path: "*", options?: Omit<ClientRouteOptions, "exact">): RouteDescriptor<"*", {}, {}>;
function createRoute(path: `${string}/*`, options?: Omit<ClientRouteOptions, "exact">): RouteDescriptor<string, {}, {}>;
function createRoute(options?: Omit<ClientRouteOptions, "exact">): RouteDescriptor<"*", {}, {}>;
function createRoute(): RouteDescriptor<"*", {}, {}>;
```

Client-safe route factory — returns a `RouteDescriptor` with `path`,
`validate`, `href()`, `.Link`, `.useParams()`, and `.useSearchParams()`
— but no `.Route`.

Use this in shared route definition files that are imported by
both server components and client components.

```ts
// routes.ts (shared)
import { createRoute } from "@lazarv/react-server/navigation";
import { z } from "zod";

export const user = createRoute("/user/[id]", {
  validate: { params: z.object({ id: z.string() }) },
});

// Client component:
const params = user.useParams();        // typed!
user.href({ id: "42" });               // → "/user/42"
<user.Link params={{ id: "42" }}>User 42</user.Link>
```

### `Form`

```ts
function Form(props: FormProps): React.JSX.Element;
```

A component that renders a form element that navigates to the current route using form data as the query parameters.

**Parameters**

- `props` — The props for the component

**Returns** — The form element

```tsx
import { Form } from '@lazarv/react-server/navigation';

export default function App() {
 return (
  <Form>
    <input type="text" name="name" />
    <button type="submit">Submit</button>
  </Form>
 );
}
```

### `Link`

```ts
function Link<T extends string>(props: LinkProps<T>): React.JSX.Element;
```

A component that renders an anchor element that links to a route.

**Parameters**

- `props` — The props for the component

**Returns** — The anchor element

```tsx
import { Link } from '@lazarv/react-server/navigation';

export default function App() {
 return (
  <Link to="/todos">Todos</Link>
 );
}
```

### `ReactServerComponent`

```ts
function ReactServerComponent(props: ReactServerComponentProps): React.JSX.Element;
```

A component that renders a server component. The component will be rendered on the server and hydrated on the client.

**Parameters**

- `props` — The props for the component

**Returns** — The server component

```tsx
import { ReactServerComponent } from '@lazarv/react-server/navigation';

export default function App() {
  return (
    <ReactServerComponent url="/todos" outlet="todos" defer />
  );
}
```

### `redirect`

```ts
function redirect(url: string, options?: {
    replace?: boolean;
}): never;
```

Perform a client-side redirect by throwing a `RedirectError`.
Must be called during render inside a component wrapped by a `Route`
(which automatically includes a `RedirectBoundary`).

The `RedirectBoundary` catches the error and uses the full navigation
system to perform the redirect, supporting both client-only and server routes.

**Parameters**

- `url` — The URL to redirect to
- `options` — Options. `replace` defaults to `true`.

```tsx
"use client";
import { redirect } from '@lazarv/react-server/navigation';

export default function ProtectedPage() {
  if (!isAuthenticated) {
    redirect("/login");
  }
  return <div>Secret content</div>;
}
```

### `Refresh`

```ts
function Refresh(props: RefreshProps): React.JSX.Element;
```

A component that triggers a refresh of the current route.

**Parameters**

- `props` — The props for the component

**Returns** — The anchor element

```tsx
import { Refresh } from '@lazarv/react-server/navigation';

export default function App() {
 return (
  <Refresh>Refresh</Refresh>
 );
}
```

### `registerRouteResources`

```ts
function registerRouteResources(path: string, resources: (RouteResourceBinding | RouteResource)[]): () => void;
```

Register resource bindings for a route path on the client side.

When a client-only navigation matches the path, the registered resources
are loaded in parallel **before** the component renders — eliminating the
waterfall that would occur if the component's `.use()` call triggered
the loader.

Call this at module level in a `"use client"` module (alongside your
client-side resource loaders).

**Parameters**

- `path` — Route path pattern (e.g. `"/todos"`)
- `resources` — Array of resource bindings (from `.from()`) or bare descriptors

**Returns** — Cleanup function that unregisters the bindings

```ts
"use client";
import { registerRouteResources } from "@lazarv/react-server/navigation";
import { todos } from "./resources.client";

registerRouteResources("/todos", [
  todos.from((_, search) => ({ filter: search.filter ?? "all" })),
]);
```

### `registerScrollContainer`

```ts
function registerScrollContainer(id: string, element: HTMLElement): void;
```

Register a scrollable container element for automatic scroll position
save/restore alongside the window scroll.

**Parameters**

- `id` — A unique, stable identifier for this container (e.g. `"sidebar"`).
- `element` — The scrollable DOM element.

### `ScrollRestoration`

```ts
function ScrollRestoration(props?: ScrollRestorationProps): null;
```

Provides automatic scroll restoration for client-side navigations.

- On **forward navigation** (link clicks): scrolls to top
- On **back/forward** (popstate): restores the saved scroll position
- Saves scroll positions to `sessionStorage` so they survive page reloads
- Automatically respects `prefers-reduced-motion` when `behavior="smooth"`

Place this component once at the top level of your app.

```tsx
"use client";
import { ScrollRestoration } from '@lazarv/react-server/navigation';

export default function App() {
  return (
    <>
      <ScrollRestoration />
      <nav>...</nav>
      <main>...</main>
    </>
  );
}
```

```tsx
Smooth scrolling
```tsx
<ScrollRestoration behavior="smooth" />
```
```

### `unregisterScrollContainer`

```ts
function unregisterScrollContainer(id: string): void;
```

Unregister a scrollable container previously registered with
`registerScrollContainer`.

### `useLocation`

```ts
function useLocation(outlet?: string): Location | null;
```

A hook that returns the current location.

**Parameters**

- `outlet` — which outlet to watch (optional, defaults to root)

**Returns** — The current location

### `useMatch`

```ts
function useMatch<T = Record<string, string>>(path: string, options?: ClientMatchOptions): T | null;
```

A client-side hook that matches a route path pattern against the current pathname.
Returns the matched params or null if no match.

This is the isomorphic counterpart to the server-side `useMatch` from `@lazarv/react-server/router`.
Works in "use client" components.

**Parameters**

- `path` — Route path pattern (e.g. "/users/[id]")
- `options` — Match options

**Returns** — The matched route params, or null

```tsx
"use client";
import { useMatch } from '@lazarv/react-server/navigation';

export default function UserProfile() {
  const params = useMatch('/users/[id]');
  if (!params) return null;
  return <p>User: {params.id}</p>;
}
```

### `useNavigate`

```ts
function useNavigate(): NavigateFunction;
```

Returns a `navigate` function that accepts a URL string or a route descriptor.

**Returns** — A navigate function.

```tsx
import { useNavigate } from '@lazarv/react-server/navigation';

const navigate = useNavigate();
navigate('/about');
navigate(products, { search: { sort: 'price' } });
```

### `useNavigationGuard`

```ts
function useNavigationGuard(guard: NavigationGuard, options?: NavigationGuardOptions): void;
```

React hook to register a navigation guard for the lifetime of the component.

The guard callback is called before every client-side navigation.
Return `false` to block navigation, a `string` to redirect, or `true`/`undefined` to allow.

Use the `beforeUnload` option to also show the browser's native "Leave site?" dialog
when the user tries to close the tab or navigate away externally.

For pattern matching, use `useMatch()` inside the guard handler rather than a
pattern parameter — this is more composable.

**Parameters**

- `guard` — The guard callback
- `options` — Options for the guard

```tsx
"use client";
import { useNavigationGuard } from '@lazarv/react-server/navigation';

// Leave guard with beforeunload support
export default function Editor() {
  const [dirty, setDirty] = useState(false);

  useNavigationGuard(
    (from, to) => {
      if (dirty) {
        return confirm("You have unsaved changes. Leave?");
      }
    },
    { beforeUnload: dirty }
  );

  return <textarea onChange={() => setDirty(true)} />;
}
```

```tsx
// Enter guard — redirect unauthenticated users
useNavigationGuard((from, to) => {
  if (!isAuthenticated && to.startsWith("/dashboard")) {
    return "/login";
  }
});
```

### `usePathname`

```ts
function usePathname(outlet?: string): string | null;
```

A hook that returns the current pathname.

**Parameters**

- `outlet` — which outlet to watch (optional, defaults to root)

**Returns** — The current pathname

### `useRouteMatch`

```ts
function useRouteMatch<TPath extends string, TParams, TSearch>(route: RouteDescriptor<TPath, TParams, TSearch>): TParams | null;
```

Test if a route matches the current pathname and return typed params (or null).

**Parameters**

- `route` — A route created by `createRoute`.

**Returns** — Matched params or null.

```tsx
import { useRouteMatch } from "@lazarv/react-server/navigation";
import { user } from "./routes";

const match = useRouteMatch(user);
if (match) console.log(match.id);
```

### `useRouteParams`

```ts
function useRouteParams<TPath extends string, TParams, TSearch>(route: RouteDescriptor<TPath, TParams, TSearch>): TParams | null;
```

Read typed, validated params for a route.
Uses the route's `validate.params` schema (if provided) to parse the raw match.

**Parameters**

- `route` — A route created by `createRoute`.

**Returns** — Parsed params, or `null` if the route doesn't match / validation fails.

```tsx
import { useRouteParams } from "@lazarv/react-server/navigation";
import { user } from "./routes";

const { id } = useRouteParams(user);  // id: string
```

### `useRouteSearchParams`

```ts
function useRouteSearchParams<TPath extends string, TParams, TSearch>(route: RouteDescriptor<TPath, TParams, TSearch>): TSearch;
```

Read typed, validated search params for a route.
Uses the route's `validate.search` schema (if provided) to parse query params.

**Parameters**

- `route` — A route created by `createRoute` with `validate.search`.

**Returns** — Parsed search params.

```tsx
import { useRouteSearchParams } from "@lazarv/react-server/navigation";
import { products } from "./routes";

const { sort, page } = useRouteSearchParams(products);
```

### `useScrollContainer`

```ts
function useScrollContainer(id: string, ref: React.RefObject<HTMLElement>): void;
```

Hook that registers a scrollable container element for automatic scroll
position save/restore. Handles registration on mount and cleanup on unmount.

**Parameters**

- `id` — A unique, stable identifier for this container (e.g. `"sidebar"`).
- `ref` — A React ref pointing to the scrollable container element.

```tsx
"use client";
import { useRef } from "react";
import { useScrollContainer } from "@lazarv/react-server/navigation";

export function Sidebar() {
  const ref = useRef<HTMLElement>(null);
  useScrollContainer("sidebar", ref);
  return <nav ref={ref} style={{ overflow: "auto", height: "100vh" }}>...</nav>;
}
```

### `useScrollPosition`

```ts
function useScrollPosition(handler: (params: ScrollPositionParams) => ScrollPosition | false | undefined | null): void;
```

Register a per-route scroll position handler.

The handler is called on every navigation with `{ to, from, savedPosition }`.
Return `{ x, y }` to scroll to a custom position, `false` to skip scrolling
entirely (useful for modal routes), or `undefined`/`null` to fall back to the
default behavior.

Call this hook from any client component — only the most recently registered
handler is active. The handler is automatically unregistered on unmount.

```tsx
"use client";
import { useScrollPosition } from "@lazarv/react-server/navigation";

export function ScrollConfig() {
  useScrollPosition(({ to }) => {
    if (to.startsWith("/modal")) return false;
    return undefined; // default behavior
  });
  return null;
}
```

### `useSearchParams`

```ts
function useSearchParams(outlet?: string): Record<string, string | string[]> | null;
```

A hook that returns the current search parameters as a plain object.
Multi-value keys are returned as arrays.

**Parameters**

- `outlet` — which outlet to watch (optional, defaults to root)

**Returns** — The current search parameters as `{ [key]: string | string[] }`, or `null`

## Classes

### `RedirectBoundary`

```ts
class RedirectBoundary extends React.Component<React.PropsWithChildren> {
}
```

Error boundary that catches `RedirectError` thrown by client-side `redirect()`.
Uses the proper navigation system to perform the redirect.

Automatically wrapped around route content by `Route` — you typically
don't need to use this directly.

```tsx
import { RedirectBoundary } from '@lazarv/react-server/navigation';

<RedirectBoundary>
  <ProtectedContent />
</RedirectBoundary>
```

### `RedirectError`

```ts
class RedirectError extends Error {
    url: string;
    replace: boolean;
    constructor(url: string, options?: {
        replace?: boolean;
    });
}
```

Error class thrown by client-side `redirect()`.
Caught by `RedirectBoundary` which uses the proper navigation system
to perform the redirect.

## Interfaces

### `NavigateFunction`

```ts
interface NavigateFunction {
    (url: string, options?: Parameters<NavigateToUrl>[1]): Promise<void>;
    <TPath extends string, TParams, TSearch>(route: RouteDescriptor<TPath, TParams, TSearch>, options?: RouteNavigateOptions<TParams, TSearch>): Promise<void>;
}
```

Navigate function returned by `useNavigate()`.

Accepts either:
- A URL string + optional nav options (classic mode)
- A route descriptor + options with typed `params` and `search` (typed mode)

In typed mode, `search` is **merged** with the current URL search params.

```tsx
const navigate = useNavigate();

// Classic — plain URL
navigate("/about");

// Typed — route with search params (merged with current URL)
navigate(products, { search: { sort: "price", page: 2 } });

// Typed — route with path params + search
navigate(user, { params: { id: "42" }, search: { tab: "posts" } });
```

### `NavigationGuardOptions`

```ts
interface NavigationGuardOptions {
    beforeUnload?: boolean;
}
```

Options for `useNavigationGuard`.

### `RouteNavigateOptions`

```ts
interface RouteNavigateOptions<TParams = {}, TSearch = {}> {
    params?: TParams;
    search?: Partial<TSearch> | ((prev: TSearch) => TSearch);
    outlet?: string;
    push?: boolean;
    rollback?: number;
    signal?: AbortSignal;
    fallback?: React.ReactNode;
    Component?: React.ReactNode;
}
```

Options when navigating to a route descriptor

### `ScrollPosition`

```ts
interface ScrollPosition {
    x: number;
    y: number;
}
```

The position passed to or returned from a scroll position handler.

### `ScrollPositionParams`

```ts
interface ScrollPositionParams {
    to: string;
    from: string | null;
    savedPosition: ScrollPosition | null;
}
```

Parameters passed to the `useScrollPosition` handler callback.

### `ScrollRestorationProps`

```ts
interface ScrollRestorationProps {
    behavior?: ScrollBehavior;
}
```

## Types

### `ClientMatchOptions`

```ts
type ClientMatchOptions = {
    exact?: boolean;
};
```

Options for the client-side useMatch hook.

### `FormProps`

```ts
type FormProps = Exclude<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> | Exclude<LinkProps<string>, "to">, "action" | "method" | "search" | "to">;
```

The props for the `Form` component.

### `LinkProps`

```ts
type LinkProps<T> = React.PropsWithChildren<{
    to: T;
    search?: Record<string, unknown> | ((prev: Record<string, string | string[]>) => Record<string, unknown>);
    target?: string;
    local?: boolean;
    root?: boolean;
    transition?: boolean;
    push?: boolean;
    replace?: boolean;
    prefetch?: boolean;
    ttl?: number;
    revalidate?: boolean | number | ((context: {
        outlet: string;
        url: string;
        timestamp: number;
    }) => Promise<boolean> | boolean);
    rollback?: number;
    noCache?: boolean;
    fallback?: React.ReactNode;
    Component?: React.ReactNode;
    onNavigate?: () => void;
    onError?: (error: unknown) => void;
}> & React.DetailedHTMLProps<React.HTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
```

The props for the `Link` component.

### `NavigationGuard`

```ts
type NavigationGuard = (from: string, to: string) => NavigationGuardResult | Promise<NavigationGuardResult>;
```

A navigation guard callback function.
Called before every client-side navigation.

**Parameters**

- `from` — The current pathname
- `to` — The target pathname

**Returns** — Whether to allow the navigation, block it, or redirect

### `NavigationGuardResult`

```ts
type NavigationGuardResult = boolean | string | undefined;
```

The result type returned by a navigation guard callback.
- `true` or `undefined`: allow navigation
- `false`: block navigation
- `string`: redirect to that URL instead

### `ReactServerComponentProps`

```ts
type ReactServerComponentProps = React.PropsWithChildren<{
    url?: string;
    outlet: string;
    defer?: boolean;
}>;
```

The props for the `ReactServerComponent` component.

### `RefreshProps`

```ts
type RefreshProps = React.PropsWithChildren<{
    url?: string;
    target?: string;
    local?: boolean;
    root?: boolean;
    transition?: boolean;
    prefetch?: boolean;
    ttl?: number;
    revalidate?: boolean | number | ((context: {
        outlet: string;
        url: string;
        timestamp: number;
    }) => Promise<boolean> | boolean);
    noCache?: boolean;
    fallback?: React.ReactNode;
    Component?: React.ReactNode;
    onRefresh?: () => void;
    onError?: (error: unknown) => void;
}> & React.DetailedHTMLProps<React.HTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
```

The props for the `Refresh` component.
