{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "nav-rail",
  "type": "registry:component",
  "title": "Nav Rail",
  "description": "A collapsible sidebar of destinations that keeps one active pill sliding between rows, and folds to a column of icons with tooltips.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [
    "https://panelui.dev/r/web/sheet.json"
  ],
  "files": [
    {
      "path": "components/panelui/nav-rail/index.tsx",
      "type": "registry:component",
      "target": "@components/panelui/nav-rail/index.tsx",
      "content": "'use client';\n\n/**\n * NavRail — the destinations of a product down one side, folded to icons when\n * the screen is narrow or the work needs the room.\n *\n * ```tsx\n * <NavRail aria-label=\"Main\">\n *   <NavRail.Header>\n *     <NavRail.Brand icon={<LayersIcon />}>Meridian</NavRail.Brand>\n *     <NavRail.Trigger />\n *   </NavRail.Header>\n *   <NavRail.Group label=\"Workspace\">\n *     <NavRail.Item href=\"/\" active icon={<HomeIcon />}>Overview</NavRail.Item>\n *     <NavRail.Item href=\"/inbox\" icon={<InboxIcon />}>\n *       Inbox <NavRail.Badge>12</NavRail.Badge>\n *     </NavRail.Item>\n *   </NavRail.Group>\n *   <NavRail.Footer>…</NavRail.Footer>\n * </NavRail>\n * ```\n *\n * ## One pill, and it travels\n *\n * The chosen destination sits on a tonal pill in the primary colour, with a\n * short accent on its leading edge. There is one pill for the whole rail, not\n * one per group, so it crosses the gap between groups when the choice does:\n * going from Inbox to Settings reads as one move down the rail rather than as\n * one row dimming while another lights.\n *\n * The pill is placed by measuring the active row in a layout effect, and kept\n * on it with a `ResizeObserver` and a `MutationObserver` as the rail changes\n * width, a label wraps or the active item changes. It is measured before paint\n * and does not animate on the first placement, so a page never opens with the\n * pill sliding in from the corner. With reduced motion the travel is skipped\n * and it is simply there.\n *\n * ## Folded, it is still a rail\n *\n * Collapsed, the rows become 40px icon squares. The active pill becomes the\n * square, the group labels become dividers, and a badge becomes a dot on the\n * corner of its icon rather than disappearing with the label. The rail's width\n * follows the state, so an app can put it in a flex row and let the content\n * take the room it gives up.\n *\n * A folded item keeps its accessible name through `label` (or its text\n * children when they are plain text), and shows the same name in a small\n * flyout on hover and focus. The visible label is `aria-hidden` while folded,\n * so a screen reader hears the row once rather than twice.\n *\n * ## Nested destinations\n *\n * A destination can own a child list with `NavRail.Sub`. The parent stays an\n * ordinary row — a link when it has an `href` — and the list is indented under\n * it, with a disclosure button that says whether it is open. Zoomed in, the\n * child rows are part of the rail's single pill and its arrow-key order; when\n * the child that is active is folded away inside a closed list, the parent\n * holds the pill so the rail still shows a place.\n *\n * Folded, the same list becomes a flyout to the side of the rail, opened by\n * hover or focus and kept out of the rail's clipping so a narrow column can\n * still show names. The parent is not the flyout's trigger in the sense a menu\n * is: the child rows are links in a real `<ul>`, and the button beside the\n * parent is a disclosure with `aria-expanded` and `aria-controls`. One level is\n * supported; a child list is not meant to contain another `NavRail.Sub`, and\n * deeper trees are the caller's own component.\n *\n * The open state is the parent's, not the rail's: `NavRail.Sub` takes\n * `defaultOpen`, or `open` and `onOpenChange` when the app holds it, and it\n * does not change when the rail folds. An open list becomes a pinned flyout\n * folded, and hover opens one without changing that state.\n *\n * ## Two weights\n *\n * `variant=\"tonal\"`, the default, keeps the current place as a quiet primary\n * tint with a short accent bar — the right volume for a rail that is part of\n * the app's chrome and is on screen the whole time. `variant=\"solid\"` fills the\n * row with the primary colour and sets its text in `primary-foreground`, the\n * same weight Tabs, Toolbar and Inspector give their chosen item, for an app\n * that wants the rail to speak as loudly as the rest of the collection. The\n * choice applies folded as well: the icon square, the count dot and the active\n * child in a flyout follow it.\n *\n * ## The keyboard\n *\n * Every row is an ordinary tab stop, so Tab walks the rail the way it walks\n * the rest of the page. Once focus is on a row, Up and Down move it, wrapping\n * at the ends and stepping over disabled rows; Home and End go to the first and\n * last visible row, skipping children hidden in a closed list. On a parent,\n * Right opens a closed list or steps into an open one, Left closes it, and\n * Escape closes it wherever focus is inside it. `NavRail.Trigger` is a button\n * and says whether the rail is open with `aria-expanded`.\n *\n * ## It does not collapse itself\n *\n * There is no viewport-width breakpoint here. Choosing a width on the client\n * after hydration makes a server-rendered page jump, and choosing one on the\n * server guesses the reader's window. The rail is controlled or uncontrolled\n * by `collapsed`, so the app decides, and can pass a media query or a stored\n * preference without the component having an opinion about either.\n */\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type ComponentProps,\n  type CSSProperties,\n  type FocusEvent as ReactFocusEvent,\n  type KeyboardEvent,\n  type MouseEvent as ReactMouseEvent,\n  type PointerEvent as ReactPointerEvent,\n  type ReactElement,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type { ElementType as RenderElementType } from 'react';\nimport { Sheet } from '@/components/panelui/sheet';\nimport { indicatorBox, isRovingKey, railWidth, rovingRow, activeRowIndex, subKeyAction, textOf, type RailRowState } from './rail';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\n/** How loudly the active row is drawn: a quiet tint, or the primary fill. */\nexport type NavRailVariant = 'tonal' | 'solid';\n\n/** What folding does: to icons, right out of the layout, or nothing at all. */\nexport type NavRailCollapsible = 'icon' | 'offcanvas' | 'none';\n\n/** Which edge of the shell the rail sits on. */\nexport type NavRailSide = 'start' | 'end';\n\n/** One curve for the width, the pill and the labels, so they move as one thing. */\nconst EASE = 'ease-[cubic-bezier(0.32,0.72,0,1)]';\n\n/**\n * The rail's surface. `--muted` is a translucent tint in the theme, so it is\n * laid over an opaque page colour instead of being mixed with it: that keeps\n * the header and the footer able to cover the rows that scroll beneath them.\n */\nconst SURFACE = 'bg-background bg-[linear-gradient(var(--muted),var(--muted))]';\n\nconst navRailVariants = tv({\n  slots: {\n    root: [\n      // Above the page beside it, so a folded label or flyout is never covered\n      // by the inset.\n      'group/nav-rail relative isolate z-10 flex h-full w-(--nav-rail-width) shrink-0 flex-col gap-1 overflow-y-auto overscroll-contain rounded-2xl p-2',\n      `${SURFACE} text-foreground ring-1 ring-foreground/[0.045] dark:ring-foreground/[0.08]`,\n      `transition-[width,opacity,visibility] duration-300 ${EASE} motion-reduce:transition-none`,\n      '[scrollbar-width:thin]',\n      // Folded right out of the layout, the rail keeps no width, padding or\n      // surface, and leaves the tab order once the fade ends. An outside\n      // trigger is what brings it back.\n      'data-[collapsible=offcanvas]:data-[collapsed]:invisible data-[collapsible=offcanvas]:data-[collapsed]:w-0 data-[collapsible=offcanvas]:data-[collapsed]:gap-0 data-[collapsible=offcanvas]:data-[collapsed]:p-0 data-[collapsible=offcanvas]:data-[collapsed]:opacity-0 data-[collapsible=offcanvas]:data-[collapsed]:ring-0 data-[collapsible=offcanvas]:data-[collapsed]:pointer-events-none',\n      // In a drawer, the surface is the sheet: no width, no rounding, no ring.\n      'data-[mobile]:h-full data-[mobile]:w-full data-[mobile]:rounded-none data-[mobile]:p-3 data-[mobile]:ring-0',\n    ],\n    header: [\n      `sticky top-0 z-20 flex shrink-0 items-center gap-2 rounded-t-[inherit] px-0.5 pb-1 ${SURFACE}`,\n      'group-data-[collapsed]/nav-rail:flex-col group-data-[collapsed]/nav-rail:gap-1 group-data-[collapsed]/nav-rail:pb-0',\n    ],\n    content: 'flex min-h-0 flex-col gap-0.5',\n    rail: [\n      'absolute inset-y-6 z-30 hidden w-2 touch-none cursor-ew-resize rounded-full bg-transparent outline-none',\n      'transition-colors duration-150 hover:bg-primary/30 focus-visible:bg-primary/30 motion-reduce:transition-none',\n      'group-data-[side=start]/nav-rail:end-0 group-data-[side=end]/nav-rail:start-0',\n      'group-data-[collapsible=offcanvas]/nav-rail:hidden group-data-[mobile]/nav-rail:hidden',\n      'sm:block',\n    ],\n    brand: [\n      'group/brand flex min-w-0 flex-1 items-center gap-2.5 rounded-[10px] px-1.5 py-1 text-start outline-none',\n      'transition-colors duration-150',\n      'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',\n      'group-data-[collapsed]/nav-rail:flex-none group-data-[collapsed]/nav-rail:justify-center group-data-[collapsed]/nav-rail:gap-0 group-data-[collapsed]/nav-rail:px-0',\n    ],\n    brandIcon:\n      'flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary [&_img]:size-full [&_img]:rounded-[inherit] [&_img]:object-cover [&_svg]:size-[18px]',\n    brandLabel: [\n      'min-w-0 max-w-40 truncate text-sm font-semibold tracking-[-0.01em]',\n      `transition-[opacity,translate,max-width] duration-300 ${EASE} motion-reduce:transition-none`,\n      'group-data-[collapsed]/nav-rail:max-w-0 group-data-[collapsed]/nav-rail:-translate-x-1 group-data-[collapsed]/nav-rail:pointer-events-none group-data-[collapsed]/nav-rail:opacity-0',\n    ],\n    trigger: [\n      'inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-lg text-muted-foreground outline-none',\n      'transition-[background-color,color,scale] duration-150 hover:bg-foreground/[0.06] hover:text-foreground',\n      'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset active:scale-95 motion-reduce:transition-none',\n    ],\n    triggerGlyph: `size-4 transition-transform duration-300 ${EASE} motion-reduce:transition-none`,\n    group: 'flex flex-col gap-0.5',\n    // The heading and the divider share the row: one fades out as the other\n    // fades in, so a group keeps its height folded and open.\n    groupLabel: 'relative flex h-7 shrink-0 items-center overflow-hidden px-2.5',\n    groupLabelText: [\n      'min-w-0 flex-1 truncate text-[10px] font-semibold tracking-[0.09em] text-muted-foreground/70 uppercase',\n      'transition-opacity duration-200 motion-reduce:transition-none',\n      'group-data-[collapsed]/nav-rail:pointer-events-none group-data-[collapsed]/nav-rail:opacity-0',\n    ],\n    groupLabelDivider: [\n      'pointer-events-none absolute inset-x-2 h-px rounded-full bg-border opacity-0',\n      'transition-opacity duration-200 motion-reduce:transition-none',\n      'group-data-[collapsed]/nav-rail:opacity-100',\n    ],\n    item: [\n      'group/item relative z-10 flex h-10 w-full shrink-0 cursor-pointer items-center gap-2.5 rounded-[10px] px-2.5 text-sm font-medium outline-none select-none',\n      'text-muted-foreground transition-colors duration-150 hover:bg-foreground/[0.05] hover:text-foreground',\n      'data-active:text-foreground data-active:hover:bg-foreground/[0.03]',\n      'data-[holding]:text-foreground data-[holding]:hover:bg-foreground/[0.03]',\n      'data-[expanded]:bg-foreground/[0.05] data-[expanded]:text-foreground',\n      // The footer is an opaque, sticky surface above the shared pill, so an\n      // active destination there carries its own fill instead.\n      'group-data-[footer-active]/nav-rail:not-data-[solid-active]:data-active:bg-primary/[0.12] group-data-[footer-active]/nav-rail:not-data-[solid-active]:data-active:hover:bg-primary/[0.12]! group-data-[footer-active]/nav-rail:not-data-[solid-active]:data-active:ring-1 group-data-[footer-active]/nav-rail:not-data-[solid-active]:data-active:ring-primary/20 group-data-[footer-active]/nav-rail:not-data-[solid-active]:data-active:ring-inset',\n      // A parent holding the pill for a closed or folded child is marked the\n      // same way: the footer's shared indicator is hidden, so it needs its own.\n      'group-data-[footer-active]/nav-rail:not-data-[solid-active]:data-[holding]:bg-primary/[0.12] group-data-[footer-active]/nav-rail:not-data-[solid-active]:data-[holding]:hover:bg-primary/[0.12]! group-data-[footer-active]/nav-rail:not-data-[solid-active]:data-[holding]:ring-1 group-data-[footer-active]/nav-rail:not-data-[solid-active]:data-[holding]:ring-primary/20 group-data-[footer-active]/nav-rail:not-data-[solid-active]:data-[holding]:ring-inset',\n      'group-data-[footer-active]/nav-rail:data-solid-active:bg-primary group-data-[footer-active]/nav-rail:data-solid-active:hover:bg-primary! group-data-[footer-active]/nav-rail:data-solid-active:ring-0',\n      'data-solid-active:text-primary-foreground! data-solid-active:hover:bg-transparent',\n      'data-[holding]:data-solid-active:text-primary-foreground!',\n      'data-disabled:pointer-events-none data-disabled:opacity-40',\n      'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',\n      'data-[icons-only]:justify-center data-[icons-only]:gap-0 data-[icons-only]:px-0',\n      'data-[nested]:h-9 data-[nested]:text-[13px]',\n      // A child in a flyout has no pill behind it, so it carries its own\n      // active weight: the same tint, ring and solid fill the rail gives the\n      // row it draws the pill over.\n      'data-[flyout]:data-active:bg-primary/[0.12] data-[flyout]:data-active:ring-1 data-[flyout]:data-active:ring-primary/20 data-[flyout]:data-active:ring-inset',\n      'data-[flyout]:data-solid-active:bg-primary data-[flyout]:data-solid-active:ring-0',\n    ],\n    icon: [\n      'flex size-5 shrink-0 items-center justify-center text-muted-foreground',\n      'transition-colors duration-150 group-hover/item:text-foreground group-data-[active]/item:text-primary',\n      'group-data-[solid-active]/item:text-primary-foreground!',\n      'group-data-[nested]/item:[&_svg]:size-4',\n      // The parent of the active child is tinted rather than pilled: it is a\n      // sign that the current page is under it, not a second current place.\n      'group-data-[child-active]/item:text-primary',\n      '[&_svg]:pointer-events-none [&_svg]:size-[18px]',\n    ],\n    label: [\n      'min-w-0 max-w-full flex-1 truncate whitespace-nowrap',\n      `transition-[opacity,translate,max-width] duration-300 ${EASE} motion-reduce:transition-none`,\n      'group-data-[icons-only]/item:max-w-0 group-data-[icons-only]/item:-translate-x-1 group-data-[icons-only]/item:pointer-events-none group-data-[icons-only]/item:opacity-0',\n    ],\n    badges: [\n      'pointer-events-none ms-auto flex shrink-0 items-center gap-1',\n      'group-data-[icons-only]/item:absolute group-data-[icons-only]/item:end-0.5 group-data-[icons-only]/item:top-0.5 group-data-[icons-only]/item:ms-0',\n      'group-data-[icons-only]/item:[&>*+*]:hidden',\n    ],\n    badge: [\n      'inline-flex h-5 min-w-5 shrink-0 items-center justify-center overflow-hidden rounded-full px-1.5',\n      'bg-foreground/[0.07] text-[11px] font-medium text-muted-foreground tabular-nums',\n      'transition-colors duration-150 group-data-[active]/item:bg-primary/15 group-data-[active]/item:text-foreground',\n      'group-data-[solid-active]/item:bg-primary-foreground/20! group-data-[solid-active]/item:text-primary-foreground!',\n      'group-data-[icons-only]/item:group-data-[solid-active]/item:bg-primary-foreground!',\n      // Folded, the count is a dot: the label is gone and a number would have\n      // nowhere to sit, but the fact that something is waiting still reads.\n      'group-data-[icons-only]/item:size-2 group-data-[icons-only]/item:min-w-0 group-data-[icons-only]/item:bg-primary group-data-[icons-only]/item:p-0 group-data-[icons-only]/item:text-[0px] group-data-[icons-only]/item:text-transparent',\n    ],\n    tooltip: [\n      'pointer-events-none fixed z-50 origin-left -translate-y-1/2',\n      'rounded-md bg-foreground px-2 py-1 text-xs font-medium whitespace-nowrap text-background shadow-md',\n    ],\n    // A parent and its child list. The wrapper is what the flyout is positioned\n    // against, and what hover and focus are read from.\n    branch: 'group/branch relative flex w-full flex-col',\n    toggle: [\n      'absolute end-1 top-5 z-20 flex size-6 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md text-muted-foreground/70 outline-none',\n      'transition-[opacity,background-color,color,scale] duration-150 hover:bg-foreground/[0.06] hover:text-foreground',\n      'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset active:scale-95 motion-reduce:transition-none',\n      'group-data-[solid-active]/branch:text-primary-foreground/80',\n    ],\n    chevron: `size-3.5 shrink-0 opacity-70 transition-transform duration-200 ${EASE} motion-reduce:transition-none`,\n    descendantDot: 'size-1.5 shrink-0 rounded-full bg-primary group-data-[icons-only]/item:hidden',\n    // Inline, the list unfolds by grid rows so its height travels rather than\n    // popping. Folded, the same element becomes the flyout beside the rail,\n    // with the tooltip's fade and scale.\n    sub: [\n      'grid grid-rows-[0fr] opacity-0 transition-[grid-template-rows,opacity] duration-200',\n      `${EASE} motion-reduce:transition-none`,\n      'data-[open]:grid-rows-[1fr] data-[open]:opacity-100',\n      'group-data-[collapsed]/nav-rail:pointer-events-none group-data-[collapsed]/nav-rail:fixed group-data-[collapsed]/nav-rail:top-0 group-data-[collapsed]/nav-rail:z-50 group-data-[collapsed]/nav-rail:grid-rows-[1fr] group-data-[collapsed]/nav-rail:origin-left group-data-[collapsed]/nav-rail:scale-95 group-data-[collapsed]/nav-rail:ps-2 group-data-[collapsed]/nav-rail:pe-0 group-data-[collapsed]/nav-rail:opacity-0 group-data-[collapsed]/nav-rail:transition-[opacity,scale] group-data-[collapsed]/nav-rail:duration-150',\n      'group-data-[side=end]/nav-rail:group-data-[collapsed]/nav-rail:ps-0 group-data-[side=end]/nav-rail:group-data-[collapsed]/nav-rail:pe-2',\n      'group-data-[collapsed]/nav-rail:data-[visible]:pointer-events-auto group-data-[collapsed]/nav-rail:data-[visible]:scale-100 group-data-[collapsed]/nav-rail:data-[visible]:opacity-100',\n    ],\n    subPanel: [\n      'min-h-0 overflow-hidden',\n      // The flyout's own surface, kept on the inner element so the outer one\n      // can stay the transition and arrow-key container.\n      'group-data-[collapsed]/nav-rail:min-w-52 group-data-[collapsed]/nav-rail:max-h-[calc(100dvh-1rem)] group-data-[collapsed]/nav-rail:overflow-y-auto group-data-[collapsed]/nav-rail:rounded-xl group-data-[collapsed]/nav-rail:border group-data-[collapsed]/nav-rail:border-border group-data-[collapsed]/nav-rail:bg-popover group-data-[collapsed]/nav-rail:p-1 group-data-[collapsed]/nav-rail:text-popover-foreground group-data-[collapsed]/nav-rail:shadow-lg',\n    ],\n    // A folded child list is shown away from the rail, where the parent's row\n    // is no longer beside it: its name and mark head the flyout, so the\n    // children are never shown without the branch they belong to.\n    subHeader: [\n      'hidden items-center gap-2 border-b border-border/60 px-2 pb-1.5 pt-1',\n      'group-data-[collapsed]/nav-rail:flex',\n    ],\n    subHeaderIcon: 'flex size-4 shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-3.5',\n    subHeaderLabel:\n      'min-w-0 truncate text-[10px] font-semibold tracking-[0.09em] text-muted-foreground uppercase',\n    subList: [\n      'flex flex-col gap-0.5',\n      // The indent and the rule are what make a child read as subordinate\n      // rather than as a row that happens to be smaller.\n      'ms-2.5 border-s border-border/60 ps-2',\n      'group-data-[collapsed]/nav-rail:ms-0 group-data-[collapsed]/nav-rail:border-s-0 group-data-[collapsed]/nav-rail:ps-0',\n    ],\n    subEntry: 'flex w-full min-w-0',\n    separator: 'mx-1.5 h-px shrink-0 rounded-full bg-border group-data-[collapsed]/nav-rail:mx-0.5',\n    footer: [\n      `sticky bottom-0 z-20 mt-auto flex shrink-0 flex-col gap-0.5 rounded-b-[inherit] pt-1 ${SURFACE}`,\n    ],\n    inset: [\n      'relative flex min-w-0 flex-1 flex-col overflow-hidden rounded-xl bg-background ring-1 ring-border/70',\n    ],\n    provider: 'group/nav-rail-shell flex min-h-svh w-full',\n    // One pill for the whole rail. The accent is its own small bar so the fill\n    // can stay a tint rather than a solid block of colour.\n    indicator: [\n      'pointer-events-none absolute top-(--indicator-y) left-(--indicator-x) z-0 h-(--indicator-h) w-(--indicator-w) rounded-[10px]',\n      'bg-primary/[0.12] opacity-0 ring-1 ring-primary/20 ring-inset',\n      'group-data-[footer-active]/nav-rail:hidden',\n      'group-data-[variant=solid]/nav-rail:bg-primary group-data-[variant=solid]/nav-rail:ring-0',\n      \"group-data-[collapsed]/nav-rail:before:hidden group-data-[variant=solid]/nav-rail:before:hidden\",\n      \"before:absolute before:inset-y-2.5 before:start-1 before:w-[3px] before:rounded-full before:bg-primary before:content-['']\",\n      'data-ready:opacity-100',\n      'data-ready:transition-[left,top,width,height] data-ready:duration-300 data-ready:ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none',\n    ],\n  },\n});\n\nexport interface NavRailContextValue {\n  /** Whether the rail is folded. In a drawer this is always `false`. */\n  collapsed: boolean;\n  variant: NavRailVariant;\n  collapsible: NavRailCollapsible;\n  side: NavRailSide;\n  /** Whether the rail is drawn as a drawer rather than in the layout. */\n  mobile: boolean;\n  /** Whether the viewport is below the shell's mobile breakpoint. */\n  isMobile: boolean;\n  /** Whether the drawer is open. */\n  mobileOpen: boolean;\n  setCollapsed(next: boolean): void;\n  setMobileOpen(open: boolean): void;\n  toggle(): void;\n}\n\nconst NavRailContext = createContext<NavRailContextValue | null>(null);\n\n/**\n * Reads the rail's state from inside a `NavRail.Provider` or a `NavRail`.\n *\n * Use it for a trigger in the page's own chrome, beside the rail: a menu\n * button in the top bar, a keyboard hint, a stored preference.\n */\nexport function useNavRail(): NavRailContextValue {\n  const context = useContext(NavRailContext);\n  if (!context) throw new Error('useNavRail must be used inside a <NavRail.Provider> or a <NavRail>.');\n  return context;\n}\n\nfunction useNavRailPart(part: string): NavRailContextValue {\n  const context = useContext(NavRailContext);\n  if (!context) throw new Error(`${part} must be used inside a <NavRail>.`);\n  return context;\n}\n\n/**\n * Whether the viewport is below a breakpoint, read in a way that is safe for a\n * server render: the server draws the wide layout, and the first client frame\n * corrects it without a hydration mismatch.\n */\nfunction useIsBelow(breakpoint: string): boolean {\n  const subscribe = useCallback(\n    (onChange: () => void) => {\n      if (typeof window === 'undefined') return () => {};\n      const query = window.matchMedia(`(max-width: ${breakpoint})`);\n      query.addEventListener('change', onChange);\n      return () => query.removeEventListener('change', onChange);\n    },\n    [breakpoint]\n  );\n  const read = useCallback(\n    () => typeof window !== 'undefined' && window.matchMedia(`(max-width: ${breakpoint})`).matches,\n    [breakpoint]\n  );\n  return useSyncExternalStore(subscribe, read, () => false);\n}\n\ninterface NavRailSubContextValue {\n  /** Whether the child list is logically open. */\n  open: boolean;\n  /** Whether it is drawn: open inline, or showing as a flyout. */\n  visible: boolean;\n  /** The id of the `<ul>`, for the disclosure's `aria-controls`. */\n  listId: string;\n  /** The parent's accessible name, used to name the list. */\n  name?: string;\n  /** The parent's mark, shown at the head of a folded flyout. */\n  icon?: ReactNode;\n  /** Whether the rail is folded, so the children live in a flyout. */\n  folded: boolean;\n  /** Opens or closes the list from the parent row. */\n  toggle(): void;\n}\n\nconst NavRailSubContext = createContext<NavRailSubContextValue | null>(null);\n\n/** True for an item drawn inside a child list, so it is indented and quieter. */\nconst NavRailNestedContext = createContext(false);\n\n/** True for an item drawn inside a folded flyout, where it keeps its label. */\nconst NavRailFlyoutContext = createContext(false);\n\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\n/**\n * Sets `inert` on an element while it is out of sight.\n *\n * Passed as a prop, a boolean would be written differently by different React\n * versions; set on the node, it is the attribute the browser reads.\n */\nfunction useInert<T extends HTMLElement>(inert: boolean) {\n  const ref = useRef<T>(null);\n  useIsomorphicLayoutEffect(() => {\n    ref.current?.toggleAttribute('inert', inert);\n  }, [inert]);\n  return ref;\n}\n\n/**\n * Whether a `NavRail.Sub`'s direct children include the active destination.\n *\n * Only the elements the caller passes are read: a child drawn by a wrapper\n * component cannot be seen here, and the parent simply goes without its\n * secondary marker. One level is the supported shape.\n */\nfunction subHasActive(sub: ReactElement): boolean {\n  const children = (sub.props as { children?: ReactNode }).children;\n  return Children.toArray(children).some(\n    (child) => isValidElement(child) && (child.props as { active?: boolean }).active === true\n  );\n}\n\n/**\n * Reads a row's state for focus movement and the pill.\n *\n * A child in a folded flyout is not part of the rail's own order: the flyout\n * has its own reading order when focus is inside it, and the pill stays on the\n * parent in the rail.\n */\nfunction railRowState(row: HTMLElement, collapsed: boolean): RailRowState {\n  const branch = row.closest<HTMLElement>('[data-slot=\"nav-rail-branch\"]');\n  const owns = branch !== null && row.parentElement === branch;\n  const sub = owns ? branch.querySelector<HTMLElement>(':scope > [data-slot=\"nav-rail-sub\"]') : null;\n  const container = row.closest<HTMLElement>('[data-slot=\"nav-rail-sub\"]');\n  const hidden = container ? collapsed || !container.hasAttribute('data-open') : false;\n  return {\n    active: row.hasAttribute('data-active'),\n    disabled: row.getAttribute('aria-disabled') === 'true' || row.hasAttribute('disabled'),\n    hidden,\n    branch: owns,\n    expanded: sub?.hasAttribute('data-open') ?? false,\n    childActive: row.hasAttribute('data-child-active'),\n    nested: container !== null,\n  };\n}\n\nexport interface NavRailProps extends Omit<ComponentProps<'nav'>, 'className'> {\n  /** Whether the rail is folded. Pass it with `onCollapsedChange` to hold the state yourself. */\n  collapsed?: boolean;\n  /** Whether it starts folded, when the caller does not hold it. Default `false`. */\n  defaultCollapsed?: boolean;\n  /** Called when the rail asks to fold or unfold. */\n  onCollapsedChange?: (collapsed: boolean) => void;\n  /** Its width open, as pixels or any CSS length. Default `15rem`. */\n  expandedWidth?: number | string;\n  /** Its width folded, as pixels or any CSS length. Default `3.5rem`. */\n  collapsedWidth?: number | string;\n  /**\n   * What folding does. `icon` keeps a column of icon squares, `offcanvas`\n   * takes the rail out of the layout entirely, and `none` keeps it open and\n   * ignores the fold entirely. Default `icon`.\n   */\n  collapsible?: NavRailCollapsible;\n  /** Which edge of the shell it sits on, for the rail handle and the drawer. Default `start`. */\n  side?: NavRailSide;\n  /**\n   * How loudly the active row is drawn. `tonal` is a quiet primary tint with an\n   * accent bar; `solid` is the primary fill and `primary-foreground` text that\n   * Tabs, Toolbar and Inspector use. Default `tonal`.\n   */\n  variant?: NavRailVariant;\n  className?: string;\n}\n\n/**\n * The rail. It is a `<nav>` landmark, so give it an `aria-label` that says\n * which set of destinations it holds when the page has more than one.\n */\nfunction NavRailRoot({\n  collapsed: collapsedProp,\n  defaultCollapsed = false,\n  onCollapsedChange,\n  expandedWidth = '15rem',\n  collapsedWidth = '3.5rem',\n  collapsible = 'icon',\n  side = 'start',\n  variant = 'tonal',\n  className,\n  style,\n  children,\n  render,\n  'aria-label': ariaLabel = 'Navigation',\n  ...props\n}: NavRailProps & RenderProp<'nav'>) {\n  const slots = navRailVariants();\n  // A `NavRail.Provider` around the rail owns the state, so the trigger in the\n  // page's top bar and the rail agree. Without one, the rail is its own owner,\n  // which keeps a single stand-alone rail as simple as it was.\n  const shared = useContext(NavRailContext);\n  const [inner, setInner] = useState(defaultCollapsed);\n  const [innerMobileOpen, setInnerMobileOpen] = useState(false);\n  const ownCollapsed = collapsedProp ?? inner;\n\n  // One live object, so a callback written on the first render still sees the\n  // state and handlers from the last one.\n  const live = useRef({ collapsed: ownCollapsed, controlled: collapsedProp !== undefined, onCollapsedChange });\n  // Written after commit, so a render React throws away never leaves its\n  // values behind for the next click to read.\n  useIsomorphicLayoutEffect(() => {\n    live.current = { collapsed: ownCollapsed, controlled: collapsedProp !== undefined, onCollapsedChange };\n  });\n\n  const setOwnCollapsed = useCallback((next: boolean) => {\n    if (!live.current.controlled) setInner(next);\n    if (next !== live.current.collapsed) live.current.onCollapsedChange?.(next);\n  }, []);\n\n  const isMobile = shared?.isMobile ?? false;\n  const mobileOpen = shared?.mobileOpen ?? innerMobileOpen;\n  const setMobileOpen = shared?.setMobileOpen ?? setInnerMobileOpen;\n  const setCollapsed = shared?.setCollapsed ?? setOwnCollapsed;\n  // In a drawer the rail is always drawn open; `none` never folds at all.\n  const mobile = isMobile && collapsible !== 'none';\n  const collapsed = mobile || collapsible === 'none' ? false : (shared?.collapsed ?? ownCollapsed);\n  const closed = collapsed;\n\n  const toggle = useCallback(() => {\n    if (mobile) setMobileOpen(!mobileOpen);\n    else if (collapsible !== 'none') setCollapsed(!closed);\n  }, [mobile, mobileOpen, setMobileOpen, collapsible, setCollapsed, closed]);\n\n  const context = useMemo<NavRailContextValue>(\n    () => ({\n      collapsed,\n      variant,\n      collapsible,\n      side,\n      mobile,\n      isMobile,\n      mobileOpen,\n      setMobileOpen,\n      setCollapsed,\n      toggle,\n    }),\n    [collapsed, variant, collapsible, side, mobile, isMobile, mobileOpen, setMobileOpen, setCollapsed, toggle]\n  );\n\n  const root = useRef<HTMLElement | null>(null);\n  // The drawer mounts its navigation after the first render, and crossing the\n  // breakpoint swaps one element for another. A callback ref is what tells the\n  // indicator to bind again when that happens, whatever caused it.\n  const [rootElement, setRootElement] = useState<HTMLElement | null>(null);\n  const setRoot = useCallback((node: HTMLElement | null) => {\n    root.current = node;\n    setRootElement(node);\n  }, []);\n  const indicator = useRef<HTMLSpanElement>(null);\n\n  /*\n   * The pill is measured rather than drawn per row: one element, moved onto\n   * whichever row is active. Its coordinates are in the rail's scrolling\n   * content, so it scrolls with the rows.\n   *\n   * A resize of the rail or any row, and a change to which row is active, both\n   * move it. The first placement is made ready on the next frame, which is\n   * what keeps it from animating out of the top-left corner on mount.\n   */\n  useIsomorphicLayoutEffect(() => {\n    const box = rootElement;\n    const pill = indicator.current;\n    if (!box || !pill) return;\n\n    let frame = 0;\n    let live = true;\n    const place = () => {\n      if (!live) return;\n      const rows = [...box.querySelectorAll<HTMLElement>('[data-slot=\"nav-rail-item\"]')];\n      const index = activeRowIndex(rows.map((row) => railRowState(row, collapsed)));\n      const active = index === null ? null : rows[index];\n      if (!active) {\n        pill.removeAttribute('data-ready');\n        box.removeAttribute('data-footer-active');\n        return;\n      }\n      // The footer is opaque and sticky, so the shared pill cannot show\n      // through it; the active row there carries its own fill instead.\n      const inFooter = active.closest('[data-slot=\"nav-rail-footer\"]') !== null;\n      if (inFooter) box.setAttribute('data-footer-active', '');\n      else box.removeAttribute('data-footer-active');\n      const rail = box.getBoundingClientRect();\n      const item = active.getBoundingClientRect();\n      // The pill is positioned against the rail's padding box, which starts\n      // inside its border, if a caller has put one there.\n      const next = indicatorBox(\n        {\n          top: rail.top + box.clientTop,\n          left: rail.left + box.clientLeft,\n          width: box.clientWidth,\n          height: box.clientHeight,\n        },\n        { top: item.top, left: item.left, width: item.width, height: item.height },\n        { x: box.scrollLeft, y: box.scrollTop }\n      );\n      pill.style.setProperty('--indicator-x', `${next.x}px`);\n      pill.style.setProperty('--indicator-y', `${next.y}px`);\n      pill.style.setProperty('--indicator-w', `${next.width}px`);\n      pill.style.setProperty('--indicator-h', `${next.height}px`);\n      if (!pill.hasAttribute('data-ready')) {\n        cancelAnimationFrame(frame);\n        frame = requestAnimationFrame(() => {\n          if (live) pill.setAttribute('data-ready', '');\n        });\n      }\n    };\n\n    const observer = new ResizeObserver(place);\n    const observe = () => {\n      observer.disconnect();\n      observer.observe(box);\n      box\n        .querySelectorAll<HTMLElement>('[data-slot=\"nav-rail-item\"]')\n        .forEach((row) => observer.observe(row));\n      // A child list opening changes the height of the rows below it without\n      // changing any row's own box, so the list itself is measured too.\n      box\n        .querySelectorAll<HTMLElement>('[data-slot=\"nav-rail-sub\"]')\n        .forEach((sub) => observer.observe(sub));\n    };\n\n    observe();\n    place();\n\n    // Which row is active, and whether a child list is open, are attributes on\n    // children rather than props of the rail, so a mutation is what tells the\n    // pill to follow. Adding or removing rows is observed for the same reason.\n    const mutations = new MutationObserver(() => {\n      observe();\n      place();\n    });\n    mutations.observe(box, {\n      childList: true,\n      subtree: true,\n      attributes: true,\n      attributeFilter: ['data-active', 'data-open'],\n    });\n\n    // A webfont arriving after the first paint changes the width of a label.\n    if (typeof document !== 'undefined' && document.fonts) {\n      document.fonts.ready.then(place).catch(() => {});\n    }\n\n    return () => {\n      live = false;\n      cancelAnimationFrame(frame);\n      observer.disconnect();\n      mutations.disconnect();\n    };\n  }, [collapsed, rootElement]);\n\n  const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n    if (event.defaultPrevented) return;\n    // A chord belongs to the browser or the system: Cmd+ArrowUp, Ctrl+Home.\n    if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n    const target = event.target;\n    if (!(target instanceof HTMLElement)) return;\n    const host = root.current;\n    if (!host) return;\n\n    const rows = [...host.querySelectorAll<HTMLElement>('[data-slot=\"nav-rail-item\"]')];\n    const toggle = target.closest<HTMLElement>('[data-slot=\"nav-rail-item-toggle\"]');\n    // The disclosure sits beside the row rather than inside it, since a link\n    // cannot contain a button. Keyboard actions on it belong to the row it\n    // belongs to.\n    const row =\n      target.closest<HTMLElement>('[data-slot=\"nav-rail-item\"]') ??\n      (toggle\n        ? (toggle\n            .closest<HTMLElement>('[data-slot=\"nav-rail-branch\"]')\n            ?.querySelector<HTMLElement>(':scope > [data-slot=\"nav-rail-item\"]') ?? null)\n        : null);\n    if (!row) return;\n    const current = rows.indexOf(row);\n    if (current < 0) return;\n\n    const states = rows.map((each) => railRowState(each, collapsed));\n    const branch = row.closest<HTMLElement>('[data-slot=\"nav-rail-branch\"]');\n    const owns = branch !== null && row.parentElement === branch;\n    const sub = owns ? branch.querySelector<HTMLElement>(':scope > [data-slot=\"nav-rail-sub\"]') : null;\n\n    // Right, Left and Escape act on the child list before the arrow keys do.\n    // The child list opens towards the inline end, which is the left in a\n    // right-to-left page, so the keys that open and close it swap with it.\n    const rtl = getComputedStyle(host).direction === 'rtl';\n    const key =\n      rtl && event.key === 'ArrowLeft'\n        ? 'ArrowRight'\n        : rtl && event.key === 'ArrowRight'\n          ? 'ArrowLeft'\n          : event.key;\n    const action = subKeyAction(key, states[current]!);\n    if (action) {\n      event.preventDefault();\n      if (action === 'open') {\n        branch?.dispatchEvent(new Event('nav-rail-sub-open'));\n        // The list renders immediately, but its `inert` comes off in the\n        // layout effect that follows, so focus waits one frame.\n        window.requestAnimationFrame(() => {\n          sub?.querySelector<HTMLElement>('[data-slot=\"nav-rail-item\"]')?.focus();\n        });\n      } else if (action === 'enter') {\n        sub?.querySelector<HTMLElement>('[data-slot=\"nav-rail-item\"]')?.focus();\n      } else {\n        // A child closes the list it sits in and puts focus back on the row\n        // that owns it; a parent closes its own list and keeps focus.\n        const container = states[current]!.nested\n          ? row.closest<HTMLElement>('[data-slot=\"nav-rail-sub\"]')\n          : null;\n        const owner = container ? container.parentElement : branch;\n        owner?.dispatchEvent(new Event('nav-rail-sub-close'));\n        if (container) {\n          owner?.querySelector<HTMLElement>(':scope > [data-slot=\"nav-rail-item\"]')?.focus();\n        }\n      }\n      return;\n    }\n\n    if (!isRovingKey(event.key)) return;\n\n    // Folded, a child list is a flyout with its own reading order: focus inside\n    // one moves within it, and focus on the rail moves over the top-level rows.\n    const inside = row.closest<HTMLElement>('[data-slot=\"nav-rail-sub\"]');\n    const scoped =\n      collapsed && inside\n        ? states.map((state, index) => ({\n            ...state,\n            hidden: rows[index]!.closest('[data-slot=\"nav-rail-sub\"]') !== inside,\n          }))\n        : states;\n    const next = rovingRow(event.key, current, scoped);\n    if (next === null) return;\n    event.preventDefault();\n    rows[next]?.focus();\n  };\n\n  // In the drawer, following a destination closes it. A row that only opens\n  // its child list stays put; the trigger closes the drawer through its own\n  // handler, which this one leaves alone.\n  const onClick = (event: ReactMouseEvent<HTMLElement>) => {\n    if (!mobile) return;\n    const target = event.target;\n    if (!(target instanceof HTMLElement)) return;\n    if (\n      target.closest(\n        '[data-slot=\"nav-rail-trigger\"], [data-slot=\"nav-rail-item-toggle\"], [data-slot=\"nav-rail-brand\"]'\n      )\n    ) {\n      return;\n    }\n    const row = target.closest<HTMLElement>('[data-slot=\"nav-rail-item\"]');\n    if (!row) return;\n    if (row.hasAttribute('data-branch') && !row.hasAttribute('href')) return;\n    setMobileOpen(false);\n  };\n\n  const nav = useRender({\n    defaultTagName: 'nav',\n    ref: setRoot,\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail',\n        'data-collapsed': collapsed ? '' : undefined,\n        'data-collapsible': collapsible,\n        'data-side': side,\n        'data-mobile': mobile ? '' : undefined,\n        'data-variant': variant,\n        'aria-label': ariaLabel,\n        className: slots.root({ className }),\n        style: {\n          ...style,\n          '--nav-rail-width': railWidth(collapsed, expandedWidth, collapsedWidth),\n        } as CSSProperties,\n        onKeyDown,\n        onClick,\n        children: (\n          <>\n            <NavRailContext.Provider value={context}>{children}</NavRailContext.Provider>\n            <span ref={indicator} aria-hidden=\"true\" data-slot=\"nav-rail-indicator\" className={slots.indicator()} />\n          </>\n        ),\n      },\n      props\n    ),\n  });\n\n  if (!mobile) return nav;\n\n  return (\n    <Sheet open={mobileOpen} onOpenChange={setMobileOpen} side={side === 'end' ? 'right' : 'left'}>\n      <Sheet.Content\n        aria-label={ariaLabel}\n        data-slot=\"nav-rail-drawer\"\n        detached={false}\n        showClose={false}\n        className=\"w-[18rem] max-w-[85vw] bg-transparent p-0 shadow-none ring-0\"\n      >\n        {nav}\n      </Sheet.Content>\n    </Sheet>\n  );\n}\nNavRailRoot.displayName = 'NavRail';\n\nexport interface NavRailProviderProps extends Omit<ComponentProps<'div'>, 'className'> {\n  /** Whether the rail is folded. Pass it with `onCollapsedChange` to hold the state yourself. */\n  collapsed?: boolean;\n  /** Whether it starts folded, when the caller does not hold it. Default `false`. */\n  defaultCollapsed?: boolean;\n  /** Called when the rail asks to fold or unfold. */\n  onCollapsedChange?: (collapsed: boolean) => void;\n  /**\n   * The key that folds and unfolds the rail with the command or control\n   * modifier, or `false` for no shortcut. Default `'b'`.\n   */\n  shortcutKey?: string | false;\n  /** Where the folded state is remembered between visits. Off by default. */\n  storageKey?: string;\n  /** The viewport width below which the rail is a drawer. Default `'48rem'`. */\n  mobileBreakpoint?: string;\n  className?: string;\n}\n\n/**\n * Holds the rail's state for a whole screen, so a trigger in the page's own\n * top bar can fold it, the keyboard shortcut works, and the saved preference\n * is read once.\n *\n * It is optional: a single stand-alone `NavRail` holds its own state. Wrap the\n * rail and the content beside it when the fold belongs to the shell.\n */\nfunction NavRailProvider({\n  collapsed: collapsedProp,\n  defaultCollapsed = false,\n  onCollapsedChange,\n  shortcutKey = 'b',\n  storageKey,\n  mobileBreakpoint = '48rem',\n  className,\n  children,\n  render,\n  ...props\n}: NavRailProviderProps & RenderProp<'div'>) {\n  const slots = navRailVariants();\n  const [inner, setInner] = useState(defaultCollapsed);\n  const [mobileOpen, setMobileOpen] = useState(false);\n  const isMobile = useIsBelow(mobileBreakpoint);\n  const collapsed = collapsedProp ?? inner;\n\n  // One live object, so a callback written on the first render still sees the\n  // state and handlers from the last one.\n  const live = useRef({ collapsed, controlled: collapsedProp !== undefined, onCollapsedChange, storageKey });\n  useIsomorphicLayoutEffect(() => {\n    live.current = { collapsed, controlled: collapsedProp !== undefined, onCollapsedChange, storageKey };\n  });\n\n  // A stored preference arrives after the first paint, which is the only frame\n  // where the default is drawn. A page that needs it before paint can read the\n  // same key on the server and pass `collapsed`.\n  useEffect(() => {\n    if (!storageKey || collapsedProp !== undefined) return;\n    try {\n      const saved = window.localStorage.getItem(storageKey);\n      if (saved === 'collapsed') setInner(true);\n      else if (saved === 'open') setInner(false);\n    } catch {\n      // A blocked storage is not a reason to fail the page.\n    }\n  }, [storageKey, collapsedProp]);\n\n  const setCollapsed = useCallback((next: boolean) => {\n    if (!live.current.controlled) setInner(next);\n    if (next !== live.current.collapsed) live.current.onCollapsedChange?.(next);\n    // A controlled rail's state belongs to its owner; the saved value is only\n    // for the case where this component is the one that keeps it.\n    const key = live.current.controlled ? undefined : live.current.storageKey;\n    if (key) {\n      try {\n        window.localStorage.setItem(key, next ? 'collapsed' : 'open');\n      } catch {\n        // A blocked storage is not a reason to fail the toggle.\n      }\n    }\n  }, []);\n\n  const toggle = useCallback(() => {\n    if (isMobile) setMobileOpen((open) => !open);\n    else setCollapsed(!live.current.collapsed);\n  }, [isMobile, setCollapsed]);\n\n  // The shortcut belongs to the shell, not to focus: it works from anywhere on\n  // the page, but not from inside a field where the letter is being typed.\n  useEffect(() => {\n    if (!shortcutKey) return;\n    const key = shortcutKey.toLowerCase();\n    const onKeyDown = (event: globalThis.KeyboardEvent) => {\n      if (event.defaultPrevented) return;\n      if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;\n      if (event.key.toLowerCase() !== key) return;\n      const target = event.target;\n      if (target instanceof HTMLElement) {\n        if (target.isContentEditable) return;\n        if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') return;\n      }\n      event.preventDefault();\n      toggle();\n    };\n    window.addEventListener('keydown', onKeyDown);\n    return () => window.removeEventListener('keydown', onKeyDown);\n  }, [shortcutKey, toggle]);\n\n  const context = useMemo<NavRailContextValue>(\n    () => ({\n      collapsed,\n      variant: 'tonal',\n      collapsible: 'icon',\n      side: 'start',\n      mobile: isMobile,\n      isMobile,\n      mobileOpen,\n      setMobileOpen,\n      setCollapsed,\n      toggle,\n    }),\n    [collapsed, isMobile, mobileOpen, setCollapsed, toggle]\n  );\n\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail-provider',\n        'data-collapsed': collapsed ? '' : undefined,\n        'data-mobile': isMobile ? '' : undefined,\n        className: slots.provider({ className }),\n        children: <NavRailContext.Provider value={context}>{children}</NavRailContext.Provider>,\n      },\n      props\n    ),\n  });\n}\nNavRailProvider.displayName = 'NavRail.Provider';\n\nexport interface NavRailHeaderProps extends Omit<ComponentProps<'div'>, 'className'> {\n  className?: string;\n}\n\n/** The top of the rail: usually a brand and the trigger that folds it. */\nfunction NavRailHeader({ className, render, ...props }: NavRailHeaderProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'nav-rail-header', className: navRailVariants().header({ className }) }, props),\n  });\n}\nNavRailHeader.displayName = 'NavRail.Header';\n\nexport interface NavRailContentProps extends Omit<ComponentProps<'div'>, 'className'> {\n  className?: string;\n}\n\n/** The destinations between the header and the footer. The rail scrolls when they outgrow it. */\nfunction NavRailContent({ className, render, ...props }: NavRailContentProps & RenderProp<'div'>) {\n  useNavRailPart('NavRail.Content');\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'nav-rail-content', className: navRailVariants().content({ className }) },\n      props\n    ),\n  });\n}\nNavRailContent.displayName = 'NavRail.Content';\n\nexport interface NavRailBrandProps extends Omit<ComponentProps<'a'>, 'href' | 'className'> {\n  /** Where it goes. Without it the brand is a plain block rather than a link. */\n  href?: string;\n  /** The mark beside the name, usually an icon. It is what remains when the rail folds. */\n  icon?: ReactNode;\n  /**\n   * What a screen reader calls the brand once the name is folded away.\n   * Taken from text children when they are plain text.\n   */\n  label?: string;\n  className?: string;\n}\n\n/** The product's mark and name at the top of the rail. */\nfunction NavRailBrand({ href, icon, label, className, children, render, ...props }: NavRailBrandProps & RenderProp<'a'>) {\n  const { collapsed } = useNavRailPart('NavRail.Brand');\n  const slots = navRailVariants();\n  const name = label ?? textOf(children);\n  return useRender({\n    defaultTagName: href === undefined ? 'div' : 'a',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail-brand',\n        href,\n        'aria-label': collapsed && name ? name : undefined,\n        // Folded, the name is only in the label, and a plain `div` may not\n        // carry one. As an image it can, and its hidden children add nothing.\n        role: collapsed && name && href === undefined ? 'img' : undefined,\n        className: slots.brand({\n          className: [href !== undefined && 'cursor-pointer hover:bg-foreground/[0.04]', className],\n        }),\n        children: (\n          <>\n            {icon ? (\n              <span aria-hidden=\"true\" data-slot=\"nav-rail-brand-icon\" className={slots.brandIcon()}>\n                {icon}\n              </span>\n            ) : null}\n            <span\n              aria-hidden={collapsed || undefined}\n              data-slot=\"nav-rail-brand-label\"\n              className={slots.brandLabel()}\n            >\n              {children}\n            </span>\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nNavRailBrand.displayName = 'NavRail.Brand';\n\nexport interface NavRailTriggerProps extends Omit<ComponentProps<'button'>, 'className'> {\n  /** What it is called when the rail is folded and pressing it opens it. Default \"Expand navigation\". */\n  expandLabel?: string;\n  /** What it is called when the rail is open and pressing it folds it. Default \"Collapse navigation\". */\n  collapseLabel?: string;\n  className?: string;\n}\n\n/** Folds and unfolds the rail. It says which it will do, and whether the rail is open. */\nfunction NavRailTrigger({\n  expandLabel = 'Expand navigation',\n  collapseLabel = 'Collapse navigation',\n  className,\n  children,\n  render,\n  ...props\n}: NavRailTriggerProps & RenderProp<'button'>) {\n  const { collapsed, toggle, isMobile, mobileOpen, collapsible } = useNavRailPart('NavRail.Trigger');\n  const slots = navRailVariants();\n  const expanded = isMobile ? mobileOpen : !collapsed;\n  const trigger = useRender({\n    defaultTagName: 'button',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail-trigger',\n        'data-state': expanded ? 'open' : 'closed',\n        type: 'button',\n        'aria-expanded': expanded,\n        'aria-label': expanded ? collapseLabel : expandLabel,\n        className: slots.trigger({ className }),\n        onClick: toggle,\n        children: children ?? (\n          <svg\n            aria-hidden=\"true\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth={1.7}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className={slots.triggerGlyph({\n              className: collapsed ? 'rotate-180' : undefined,\n            })}\n          >\n            <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"3\" />\n            <path d=\"M9 4v16\" />\n            <path d=\"m15.5 9.5-2.5 2.5 2.5 2.5\" />\n          </svg>\n        ),\n      },\n      props\n    ),\n  });\n  // A rail that cannot fold has nothing for the trigger to say or do.\n  if (collapsible === 'none') return null;\n  return trigger;\n}\nNavRailTrigger.displayName = 'NavRail.Trigger';\n\nexport interface NavRailRailProps extends Omit<ComponentProps<'button'>, 'className'> {\n  /** What the handle is called. Default \"Toggle navigation\". */\n  label?: string;\n  className?: string;\n}\n\n/**\n * A thin grab handle down the rail's outer edge. It is a pointer affordance,\n * not a second control in the tab order: the rail's trigger is the one a\n * keyboard uses.\n */\nfunction NavRailRail({ label = 'Toggle navigation', className, render, ...props }: NavRailRailProps & RenderProp<'button'>) {\n  const { toggle, collapsible, mobile } = useNavRailPart('NavRail.Rail');\n  const rail = useRender({\n    defaultTagName: 'button',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail-rail',\n        type: 'button',\n        tabIndex: -1,\n        'aria-label': label,\n        title: label,\n        className: navRailVariants().rail({ className }),\n        onClick: toggle,\n      },\n      props\n    ),\n  });\n  // A drawer has its own backdrop, and a rail that cannot fold has nothing to\n  // grab.\n  if (collapsible === 'none' || mobile) return null;\n  return rail;\n}\nNavRailRail.displayName = 'NavRail.Rail';\n\nexport interface NavRailGroupProps extends Omit<ComponentProps<'div'>, 'className'> {\n  /**\n   * The heading over the group. Open, it is a small uppercase label; folded,\n   * it becomes a divider and stays available to a screen reader as the group's\n   * name.\n   */\n  label?: ReactNode;\n  className?: string;\n}\n\n/**\n * A set of destinations under one heading: the workspace, the account, the\n * things you can do to it. The heading is optional.\n */\nfunction NavRailGroup({ label, className, children, render, ...props }: NavRailGroupProps & RenderProp<'div'>) {\n  const slots = navRailVariants();\n  const labelId = useId();\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail-group',\n        role: label ? 'group' : undefined,\n        'aria-labelledby': label ? labelId : undefined,\n        className: slots.group({ className }),\n        children: (\n          <>\n            {label ? (\n              <div data-slot=\"nav-rail-group-label\" className={slots.groupLabel()}>\n                <span\n                  id={labelId}\n                  className={slots.groupLabelText()}\n                >\n                  {label}\n                </span>\n                <span aria-hidden=\"true\" className={slots.groupLabelDivider()} />\n              </div>\n            ) : null}\n            {children}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nNavRailGroup.displayName = 'NavRail.Group';\n\n/** The count in a nav rail: a number beside the label, a dot on the icon when folded. */\nfunction NavRailBadge({ className, children, render, ...props }: NavRailBadgeProps & RenderProp<'span'>) {\n  const { collapsed } = useNavRailPart('NavRail.Badge');\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail-badge',\n        'aria-hidden': collapsed || undefined,\n        className: navRailVariants().badge({ className }),\n        children,\n      },\n      props\n    ),\n  });\n}\n\nexport interface NavRailBadgeProps extends Omit<ComponentProps<'span'>, 'className'> {\n  className?: string;\n}\n\n/**\n * Pulls the badges and the child list out of an item's children.\n *\n * A badge sits beside the label and a `NavRail.Sub` becomes the row's own\n * child list; everything else is the label, which is free to truncate.\n */\nfunction splitItemChildren(children: ReactNode): {\n  label: ReactNode;\n  badges: ReactElement[];\n  sub?: ReactElement;\n} {\n  const parts = Children.toArray(children);\n  const label: ReactNode[] = [];\n  const badges: ReactElement[] = [];\n  let sub: ReactElement | undefined;\n  for (const part of parts) {\n    if (isValidElement(part) && part.type === NavRailBadge) badges.push(part);\n    else if (isValidElement(part) && part.type === NavRailSub) sub ??= part;\n    else label.push(part);\n  }\n  return { label: label.length === 1 ? label[0] : label, badges, sub };\n}\n\nexport interface NavRailItemProps extends Omit<ComponentProps<'a'>, 'href' | 'className' | 'children'> {\n  /** Where it goes. With it the row is an anchor; without it, a button. */\n  href?: string;\n  /** Whether this is the page the reader is on. Sets `aria-current=\"page\"` and lights the pill. */\n  active?: boolean;\n  /** Draws the row unavailable: it cannot be chosen, and the arrow keys step over it. */\n  disabled?: boolean;\n  /** The mark before the label: what remains, with the label, when the rail folds. */\n  icon?: ReactNode;\n  /**\n   * What a screen reader calls the row once the label is folded away, and what\n   * the folded tooltip shows. Taken from text children when they are plain text.\n   */\n  label?: string;\n  className?: string;\n  children?: ReactNode;\n}\n\n/** A small chevron for the row that owns a child list. */\nfunction NavRailChevron({ open, className }: { open: boolean; className?: string }) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.8}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={[className, open ? 'rotate-90' : undefined].filter(Boolean).join(' ')}\n    >\n      <path d=\"m9 18 6-6-6-6\" />\n    </svg>\n  );\n}\n\n/**\n * One destination: an icon, a label and optionally a `NavRail.Badge` or a\n * `NavRail.Sub` child list. It is an anchor with `href` and a button without\n * one, so a client-side router can draw links and a one-off action can be a\n * button without a second part.\n *\n * With a child list and an `href`, the row stays a link and a separate\n * disclosure button beside it carries `aria-expanded` and `aria-controls`.\n * Without an `href`, the row itself is the disclosure: pressing it opens the\n * list rather than navigating.\n */\nfunction NavRailItem({\n  href,\n  active = false,\n  disabled = false,\n  icon,\n  label,\n  className,\n  children,\n  render,\n  ...props\n}: NavRailItemProps & RenderProp<'a'>) {\n  const { collapsed, variant, side } = useNavRailPart('NavRail.Item');\n  const slots = navRailVariants();\n  const nested = useContext(NavRailNestedContext);\n  const inFlyout = useContext(NavRailFlyoutContext);\n  // A child list folded into a flyout is back on a wide surface: the children\n  // keep their labels even though the rail itself is a column of icons.\n  const iconsOnly = collapsed && !inFlyout;\n  const { label: labelChildren, badges, sub } = splitItemChildren(children);\n  const name = label ?? textOf(labelChildren);\n  // Folded, a badge is a dot hidden from assistive technology, so its count\n  // joins the row's name instead of going unannounced.\n  const badgeText = badges\n    .map((badge) => (isValidElement(badge) ? textOf((badge.props as { children?: ReactNode }).children) : undefined))\n    .filter((text): text is string => text !== undefined)\n    .join(', ');\n  const foldedName = name && badgeText ? `${name}, ${badgeText}` : name;\n  const { onClick, ...rest } = props;\n\n  const branch = sub !== undefined;\n  const rowIsToggle = branch && href === undefined;\n  // Folded, there is no disclosure arrow: the icon is the whole row. Hover and\n  // focus show the flyout, and the row still reports whether it is out. Only a\n  // branch has a list to disclose; a leaf must not point at an id that is not\n  // there.\n  const rowOwnsDisclosure = branch && (rowIsToggle || collapsed);\n  const activeDescendant = sub !== undefined && subHasActive(sub);\n\n  const listId = useId();\n  const subProps = sub ? (sub.props as NavRailSubProps) : undefined;\n  const [innerOpen, setInnerOpen] = useState(subProps?.defaultOpen ?? false);\n  const open = subProps?.open ?? innerOpen;\n  // The active destination may be a child that is not drawn: a closed list, or\n  // a folded flyout. The parent then holds the pill so the rail still shows a\n  // place, and takes the same weight as if it were the active row.\n  const holdsPill = activeDescendant && (collapsed || !open);\n  const solidActive = variant === 'solid' && (active || holdsPill);\n  // Folded, hover and focus show the flyout without changing the open state;\n  // Escape dismisses it until focus or the pointer leaves the parent.\n  const [dismissed, setDismissed] = useState(false);\n  const [hovered, setHovered] = useState(false);\n  const [focusWithin, setFocusWithin] = useState(false);\n  const container = useRef<HTMLDivElement>(null);\n  // A flyout opens on hover, but not the instant the pointer crosses a row:\n  // the wait is what keeps a sweep down the rail from flashing every branch on\n  // the way to the one the reader wants. Focus does not wait.\n  const openHover = useRef<number | null>(null);\n  const enterBranch = () => {\n    if (openHover.current !== null) window.clearTimeout(openHover.current);\n    if (!collapsed) {\n      setHovered(true);\n      return;\n    }\n    openHover.current = window.setTimeout(() => {\n      openHover.current = null;\n      setHovered(true);\n    }, 120);\n  };\n  const leaveBranch = () => {\n    if (openHover.current !== null) {\n      window.clearTimeout(openHover.current);\n      openHover.current = null;\n    }\n    setHovered(false);\n    setDismissed(false);\n  };\n  useEffect(\n    () => () => {\n      if (openHover.current !== null) window.clearTimeout(openHover.current);\n    },\n    []\n  );\n  // A folded label is placed against the viewport rather than the row: the\n  // rail is a scroll container, and one that clipped its own tooltip would\n  // show nothing beside the icons.\n  const [tooltipAt, setTooltipAt] = useState<{ top: number; left?: number; right?: number } | null>(null);\n  const hasTooltip = iconsOnly && !branch && Boolean(name);\n\n  const showTooltip = (node: HTMLElement) => {\n    if (!iconsOnly || branch || !name) return;\n    const rect = node.getBoundingClientRect();\n    const rtl = getComputedStyle(node).direction === 'rtl';\n    // The label opens on the rail's inner side: inline-end when the rail sits\n    // on the start edge, inline-start when it sits on the end.\n    const towardEnd = side === 'start';\n    const inlineEnd = rtl ? { right: window.innerWidth - rect.left + 8 } : { left: rect.right + 8 };\n    const inlineStart = rtl ? { left: rect.right + 8 } : { right: window.innerWidth - rect.left + 8 };\n    setTooltipAt({ top: rect.top + rect.height / 2, ...(towardEnd ? inlineEnd : inlineStart) });\n  };\n\n  useEffect(() => {\n    if (!tooltipAt) return;\n    const hide = () => setTooltipAt(null);\n    window.addEventListener('scroll', hide, true);\n    window.addEventListener('resize', hide);\n    return () => {\n      window.removeEventListener('scroll', hide, true);\n      window.removeEventListener('resize', hide);\n    };\n  }, [tooltipAt]);\n\n  useEffect(() => {\n    if (!iconsOnly) setTooltipAt(null);\n  }, [iconsOnly]);\n\n  const liveSub = useRef({\n    open,\n    controlled: subProps?.open !== undefined,\n    onOpenChange: subProps?.onOpenChange,\n  });\n  // Written after commit, so a render React throws away never leaves its\n  // values behind for the next click to read.\n  useIsomorphicLayoutEffect(() => {\n    liveSub.current = {\n      open,\n      controlled: subProps?.open !== undefined,\n      onOpenChange: subProps?.onOpenChange,\n    };\n  });\n\n  const setOpen = useCallback((next: boolean) => {\n    if (!liveSub.current.controlled) setInnerOpen(next);\n    if (next !== liveSub.current.open) liveSub.current.onOpenChange?.(next);\n    if (next) setDismissed(false);\n  }, []);\n  const toggleSub = useCallback(() => setOpen(!liveSub.current.open), [setOpen]);\n\n  const visible = open || (collapsed && !dismissed && (hovered || focusWithin));\n  const subContext = useMemo<NavRailSubContextValue | null>(\n    () =>\n      branch\n        ? { open, visible, listId, name, icon, folded: collapsed, toggle: toggleSub }\n        : null,\n    [branch, open, visible, listId, name, icon, collapsed, toggleSub]\n  );\n\n  // The rail's arrow keys ask the child list to open or close through an event\n  // on the branch, so the state stays here where the disclosure button is.\n  useEffect(() => {\n    const node = container.current;\n    if (!node || !branch) return;\n    const openSub = () => {\n      setDismissed(false);\n      setOpen(true);\n    };\n    const closeSub = () => {\n      setOpen(false);\n      setDismissed(true);\n    };\n    node.addEventListener('nav-rail-sub-open', openSub);\n    node.addEventListener('nav-rail-sub-close', closeSub);\n    return () => {\n      node.removeEventListener('nav-rail-sub-open', openSub);\n      node.removeEventListener('nav-rail-sub-close', closeSub);\n    };\n  }, [branch, setOpen]);\n\n  // A pinned flyout is a transient surface: a press anywhere else closes it.\n  useEffect(() => {\n    if (!branch || !collapsed || !open) return;\n    const onPointerDown = (event: PointerEvent) => {\n      const node = container.current;\n      if (!node) return;\n      if (event.target instanceof Node && node.contains(event.target)) return;\n      setOpen(false);\n    };\n    document.addEventListener('pointerdown', onPointerDown);\n    return () => document.removeEventListener('pointerdown', onPointerDown);\n  }, [branch, collapsed, open, setOpen]);\n\n  const stop = (event: ReactMouseEvent<HTMLElement>) => {\n    event.preventDefault();\n    event.stopPropagation();\n  };\n\n  const rowClick = (event: ReactMouseEvent<HTMLAnchorElement>) => {\n    if (disabled) {\n      stop(event);\n      return;\n    }\n    onClick?.(event);\n    if (event.defaultPrevented) return;\n    if (rowIsToggle) {\n      event.preventDefault();\n      toggleSub();\n    }\n  };\n\n  const row = useRender({\n    defaultTagName: href === undefined ? 'button' : 'a',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail-item',\n        'data-active': active ? '' : undefined,\n        'data-solid-active': solidActive ? '' : undefined,\n        'data-holding': holdsPill ? '' : undefined,\n        'data-disabled': disabled ? '' : undefined,\n        'data-icons-only': iconsOnly ? '' : undefined,\n        'data-nested': nested ? '' : undefined,\n        'data-flyout': inFlyout ? '' : undefined,\n        'data-branch': branch ? '' : undefined,\n        'data-child-active': activeDescendant ? '' : undefined,\n        // Folded, the flyout is a surface away from the rail; the row it\n        // belongs to stays marked while it is out, so the two read together.\n        'data-expanded': collapsed && visible ? '' : undefined,\n        'aria-current': active ? 'page' : undefined,\n        'aria-disabled': disabled || undefined,\n        'aria-label': iconsOnly && foldedName ? foldedName : undefined,\n        // What is on screen, not only what is pinned: a folded flyout shows on\n        // hover or focus while `open` stays false.\n        'aria-expanded': rowOwnsDisclosure ? visible : undefined,\n        'aria-controls': rowOwnsDisclosure ? listId : undefined,\n        href,\n        type: href === undefined ? 'button' : undefined,\n        disabled: href === undefined ? disabled : undefined,\n        tabIndex: disabled ? -1 : undefined,\n        onClick: disabled ? stop : branch ? rowClick : onClick,\n        onPointerEnter: (event: ReactPointerEvent<HTMLElement>) => showTooltip(event.currentTarget),\n        onPointerLeave: () => setTooltipAt(null),\n        onFocus: (event: ReactFocusEvent<HTMLElement>) => showTooltip(event.currentTarget),\n        onBlur: () => setTooltipAt(null),\n        className: slots.item({\n          className: [branch && !rowIsToggle && !collapsed ? 'pe-9' : undefined, className],\n        }),\n        children: (\n          <>\n            {icon ? (\n              <span aria-hidden=\"true\" data-slot=\"nav-rail-icon\" className={slots.icon()}>\n                {icon}\n              </span>\n            ) : null}\n            {/* A parent whose child is active but has no icon needs a marker of\n                its own; with an icon the tint is enough. */}\n            {activeDescendant && !icon ? (\n              <span aria-hidden=\"true\" data-slot=\"nav-rail-active-descendant\" className={slots.descendantDot()} />\n            ) : null}\n            <span aria-hidden={iconsOnly || undefined} data-slot=\"nav-rail-label\" className={slots.label()}>\n              {labelChildren}\n            </span>\n            {badges.length > 0 ? (\n              <span data-slot=\"nav-rail-badges\" className={slots.badges()}>\n                {badges}\n              </span>\n            ) : null}\n            {rowIsToggle && !collapsed ? <NavRailChevron open={open} className={slots.chevron()} /> : null}\n            {/* Folded, a branch with no mark of its own would be an empty\n                square. A centred chevron is the one affordance that still says\n                it opens a list. */}\n            {iconsOnly && branch && !icon && badges.length === 0 ? (\n              <NavRailChevron open={visible} className={slots.chevron()} />\n            ) : null}\n            {hasTooltip && tooltipAt ? (\n              <span\n                aria-hidden=\"true\"\n                data-slot=\"nav-rail-tooltip\"\n                className={slots.tooltip()}\n                style={tooltipAt}\n              >\n                {name}\n              </span>\n            ) : null}\n          </>\n        ),\n      },\n      rest\n    ),\n  });\n\n  if (!branch) return row;\n\n  return (\n    <div\n      ref={container}\n      data-slot=\"nav-rail-branch\"\n      data-solid-active={solidActive ? '' : undefined}\n      className={slots.branch()}\n      onPointerEnter={enterBranch}\n      onPointerLeave={leaveBranch}\n      onFocus={() => setFocusWithin(true)}\n      onBlur={(event) => {\n        const next = event.relatedTarget;\n        if (next instanceof Node && event.currentTarget.contains(next)) return;\n        setFocusWithin(false);\n        setDismissed(false);\n      }}\n    >\n      {row}\n      {rowIsToggle || collapsed ? null : (\n        <button\n          type=\"button\"\n          data-slot=\"nav-rail-item-toggle\"\n          data-open={open ? '' : undefined}\n          aria-expanded={open}\n          aria-controls={listId}\n          aria-label={name ? `${name} submenu` : 'Submenu'}\n          tabIndex={disabled ? -1 : undefined}\n          disabled={disabled}\n          onClick={(event) => {\n            event.preventDefault();\n            event.stopPropagation();\n            toggleSub();\n          }}\n          className={slots.toggle()}\n        >\n          <NavRailChevron open={open} className={slots.chevron()} />\n        </button>\n      )}\n      {sub ? <NavRailSubContext.Provider value={subContext}>{sub}</NavRailSubContext.Provider> : null}\n    </div>\n  );\n}\nNavRailItem.displayName = 'NavRail.Item';\n\nexport interface NavRailSubProps extends Omit<ComponentProps<'div'>, 'className' | 'children'> {\n  /**\n   * Whether the child list is open. Pass it with `onOpenChange` to hold the\n   * state yourself; it is separate from the rail's fold.\n   */\n  open?: boolean;\n  /** Whether it starts open when the caller does not hold it. Default `false`. */\n  defaultOpen?: boolean;\n  /** Called when the parent's disclosure asks to open or close the list. */\n  onOpenChange?: (open: boolean) => void;\n  /**\n   * Whether a folded flyout is headed by the parent's mark and name. Off by\n   * default: the children are the destination list, and a heading is opt-in.\n   */\n  heading?: boolean;\n  /**\n   * What a screen reader calls the child list. Defaults to the parent row's\n   * name, which is usually the right thing to hear.\n   */\n  label?: string;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * The child list under a destination: one level of nesting, indented inline\n * when the rail is open and shown as a flyout beside it when the rail is\n * folded. The children are ordinary `NavRail.Item`s, so they carry their own\n * `href`, `active` and icons.\n *\n * The list is a real `<ul>` of links, not a menu: these are destinations, and\n * the parent's disclosure button is what says whether it is open.\n */\nfunction NavRailSub({\n  open: _open,\n  defaultOpen: _defaultOpen,\n  onOpenChange: _onOpenChange,\n  heading = false,\n  label,\n  className,\n  children,\n  render,\n  ...props\n}: NavRailSubProps & RenderProp<'div'>) {\n  const sub = useContext(NavRailSubContext);\n  const rail = useNavRailPart('NavRail.Sub');\n  const slots = navRailVariants();\n  // Only the parent row reads `open`, `defaultOpen` and `onOpenChange`: it owns\n  // the disclosure state, so they are taken here to keep them off the DOM.\n  void _open;\n  void _defaultOpen;\n  void _onOpenChange;\n  // Out of sight, the list leaves the tab order and the accessibility tree.\n  const ref = useInert<HTMLDivElement>(!sub?.visible);\n\n  // Folded, the list is a flyout. It is placed against the viewport so the\n  // rail's own scroll cannot clip it, and it is put back on its parent when\n  // the page or the rail moves.\n  useIsomorphicLayoutEffect(() => {\n    const node = ref.current;\n    if (!node || !sub?.folded || !sub.visible) return;\n    const place = () => {\n      const branch = node.parentElement;\n      if (!branch) return;\n      const rect = branch.getBoundingClientRect();\n      const rtl = getComputedStyle(branch).direction === 'rtl';\n      // The list opens away from the edge the rail sits on. The two offsets\n      // are the gap from the matching viewport edge, written in the logical\n      // property so a right-to-left page reads the same way.\n      const fromInlineStart = rtl ? window.innerWidth - rect.left : rect.right;\n      const fromInlineEnd = rtl ? rect.right : window.innerWidth - rect.left;\n      // Keep the whole flyout inside the viewport, including when its parent\n      // is a footer row near the floor.\n      const highest = window.innerHeight - node.offsetHeight - 8;\n      node.style.top = `${Math.max(8, Math.min(rect.top, highest))}px`;\n      if (rail.side === 'start') {\n        node.style.insetInlineStart = `${Math.min(window.innerWidth - 8, fromInlineStart)}px`;\n        node.style.insetInlineEnd = '';\n      } else {\n        node.style.insetInlineEnd = `${Math.min(window.innerWidth - 8, fromInlineEnd)}px`;\n        node.style.insetInlineStart = '';\n      }\n    };\n    place();\n    window.addEventListener('resize', place);\n    window.addEventListener('scroll', place, true);\n    return () => {\n      window.removeEventListener('resize', place);\n      window.removeEventListener('scroll', place, true);\n    };\n  }, [ref, sub?.folded, sub?.visible, rail.side]);\n\n  if (!sub) throw new Error('NavRail.Sub must be used inside a NavRail.Item.');\n\n  const entries = Children.toArray(children).map((child, index) => (\n    <li key={index} data-slot=\"nav-rail-sub-entry\" className={slots.subEntry()}>\n      {child}\n    </li>\n  ));\n\n  return useRender({\n    defaultTagName: 'div',\n    ref,\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail-sub',\n        'data-open': sub.open ? '' : undefined,\n        'data-visible': sub.visible ? '' : undefined,\n        'data-layout': sub.folded ? 'flyout' : 'inline',\n        'aria-hidden': sub.visible ? undefined : true,\n        className: slots.sub({ className }),\n        children: (\n          <div data-slot=\"nav-rail-sub-panel\" className={slots.subPanel()}>\n            {sub.folded && heading && sub.name ? (\n              <div aria-hidden=\"true\" data-slot=\"nav-rail-sub-header\" className={slots.subHeader()}>\n                {sub.icon ? <span className={slots.subHeaderIcon()}>{sub.icon}</span> : null}\n                <span className={slots.subHeaderLabel()}>{sub.name}</span>\n              </div>\n            ) : null}\n            <ul\n              id={sub.listId}\n              data-slot=\"nav-rail-sub-list\"\n              aria-label={label ?? sub.name}\n              className={slots.subList()}\n            >\n              <NavRailNestedContext.Provider value>\n                <NavRailFlyoutContext.Provider value={sub.folded}>{entries}</NavRailFlyoutContext.Provider>\n              </NavRailNestedContext.Provider>\n            </ul>\n          </div>\n        ),\n      },\n      props\n    ),\n  });\n}\nNavRailSub.displayName = 'NavRail.Sub';\n\nexport interface NavRailInsetProps extends Omit<ComponentProps<'main'>, 'className'> {\n  className?: string;\n}\n\n/**\n * The page beside the rail: the surface the work sits on. It fills the rest of\n * the shell and keeps its own scroll, so the rail never slides away with the\n * page.\n */\nfunction NavRailInset({ className, render, ...props }: NavRailInsetProps & RenderProp<'main'>) {\n  const context = useContext(NavRailContext);\n  return useRender({\n    defaultTagName: 'main',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail-inset',\n        'data-mobile': context?.isMobile ? '' : undefined,\n        className: navRailVariants().inset({ className }),\n      },\n      props\n    ),\n  });\n}\nNavRailInset.displayName = 'NavRail.Inset';\n\nexport interface NavRailSeparatorProps extends Omit<ComponentProps<'div'>, 'className'> {\n  className?: string;\n}\n\n/** A rule between groups of destinations that belong apart. */\nfunction NavRailSeparator({ className, render, ...props }: NavRailSeparatorProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'nav-rail-separator',\n        role: 'separator',\n        'aria-orientation': 'horizontal',\n        className: navRailVariants().separator({ className }),\n      },\n      props\n    ),\n  });\n}\nNavRailSeparator.displayName = 'NavRail.Separator';\n\nexport interface NavRailFooterProps extends Omit<ComponentProps<'div'>, 'className'> {\n  className?: string;\n}\n\n/** The bottom of the rail: account, settings, help, held against the floor. */\nfunction NavRailFooter({ className, render, ...props }: NavRailFooterProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'nav-rail-footer', className: navRailVariants().footer({ className }) }, props),\n  });\n}\nNavRailFooter.displayName = 'NavRail.Footer';\n\nexport const NavRail = Object.assign(NavRailRoot, {\n  Provider: NavRailProvider,\n  Header: NavRailHeader,\n  Content: NavRailContent,\n  Brand: NavRailBrand,\n  Trigger: NavRailTrigger,\n  Rail: NavRailRail,\n  Inset: NavRailInset,\n  Group: NavRailGroup,\n  Item: NavRailItem,\n  Sub: NavRailSub,\n  Badge: NavRailBadge,\n  Separator: NavRailSeparator,\n  Footer: NavRailFooter,\n});\n\nexport {\n  NavRailProvider,\n  NavRailHeader,\n  NavRailContent,\n  NavRailBrand,\n  NavRailTrigger,\n  NavRailRail,\n  NavRailInset,\n  NavRailGroup,\n  NavRailItem,\n  NavRailSub,\n  NavRailBadge,\n  NavRailSeparator,\n  NavRailFooter,\n  navRailVariants,\n};\n"
    },
    {
      "path": "components/panelui/nav-rail/rail.ts",
      "type": "registry:component",
      "target": "@components/panelui/nav-rail/rail.ts",
      "content": "/**\n * The arithmetic behind the rail, kept out of the component so it can be read\n * and tested on its own: where the one active pill sits, how wide the rail is,\n * and which row the arrow keys go to next.\n *\n * None of this touches a DOM node. A caller reads the boxes it already has and\n * hands over the numbers, which is what makes the behaviour testable without a\n * browser and keeps the layout effect in the component to measuring alone.\n */\n\n/** A box in viewport coordinates, as `getBoundingClientRect` reports one. */\nexport interface RailRect {\n  top: number;\n  left: number;\n  width: number;\n  height: number;\n}\n\n/** Where the indicator is drawn, in the scrolling content of the rail. */\nexport interface IndicatorBox {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nexport interface RailScroll {\n  x: number;\n  y: number;\n}\n\n/**\n * Places the indicator over a row.\n *\n * The difference between the two boxes is in viewport coordinates, so the\n * rail's own scroll is added back: the indicator is an absolutely positioned\n * child of the scroller and has to travel with the rows it sits over rather\n * than stay pinned to the visible edge.\n */\nexport function indicatorBox(\n  rail: RailRect,\n  item: RailRect,\n  scroll: RailScroll = { x: 0, y: 0 }\n): IndicatorBox {\n  return {\n    x: item.left - rail.left + scroll.x,\n    y: item.top - rail.top + scroll.y,\n    width: Math.max(item.width, 0),\n    height: Math.max(item.height, 0),\n  };\n}\n\n/** A width written either as pixels or as any CSS length. */\nexport type RailWidth = number | string;\n\n/** The rail's width, with a number read as pixels. */\nexport function railWidth(collapsed: boolean, expanded: RailWidth, closed: RailWidth): string {\n  const value = collapsed ? closed : expanded;\n  return typeof value === 'number' ? `${Math.max(value, 0)}px` : value;\n}\n\nexport type RovingKey = 'ArrowDown' | 'ArrowUp' | 'Home' | 'End';\n\n/** Whether a key is one the rail answers with focus movement. */\nexport function isRovingKey(key: string): key is RovingKey {\n  return key === 'ArrowDown' || key === 'ArrowUp' || key === 'Home' || key === 'End';\n}\n\n/**\n * The row focus should move to, or `null` when the key is not ours.\n *\n * Disabled rows are stepped over rather than landed on, and the two arrows\n * wrap at the ends: a rail is a closed loop of destinations, so continuing past\n * the last one is less surprising than stopping dead. Home and End look inward\n * from the ends so a disabled first or last row does not swallow the jump.\n */\nexport function rovingIndex(key: string, current: number, disabled: readonly boolean[]): number | null {\n  const count = disabled.length;\n  if (count === 0) return null;\n  const enabled = (index: number) => disabled[index] !== true;\n  const inside = current >= 0 && current < count;\n\n  if (key === 'Home' || key === 'End') {\n    const step = key === 'Home' ? 1 : -1;\n    let index = key === 'Home' ? 0 : count - 1;\n    for (let moves = 0; moves < count; moves += 1) {\n      if (enabled(index)) return index;\n      index += step;\n    }\n    return null;\n  }\n\n  if (key !== 'ArrowDown' && key !== 'ArrowUp') return null;\n  const step = key === 'ArrowDown' ? 1 : -1;\n  let index = inside ? current : step > 0 ? -1 : count;\n  for (let moves = 0; moves < count; moves += 1) {\n    index = (index + step + count) % count;\n    if (enabled(index)) return index;\n  }\n  return null;\n}\n\n/**\n * What the rail needs to know about one row to move focus and the pill.\n *\n * The component reads these off the DOM; keeping the shape here is what makes\n * \"which rows are reachable\" and \"where does the pill go\" testable without a\n * browser.\n */\nexport interface RailRowState {\n  /** It is the destination the reader is on. */\n  active: boolean;\n  /** It cannot be chosen, and the arrow keys step over it. */\n  disabled: boolean;\n  /** It is not drawn: inside a closed sub-list, or in a folded flyout that is away. */\n  hidden: boolean;\n  /** It owns a child list. */\n  branch: boolean;\n  /** Its child list is open. */\n  expanded: boolean;\n  /** One of its children is the active destination. */\n  childActive: boolean;\n  /** It sits inside a child list rather than at the top of the rail. */\n  nested: boolean;\n}\n\n/** The indexes of the rows that are drawn, in the order they are read. */\nexport function visibleIndexes(rows: readonly RailRowState[]): number[] {\n  return rows.flatMap((row, index) => (row.hidden ? [] : [index]));\n}\n\n/**\n * The row focus should move to, or `null` when there is nowhere to go.\n *\n * Hidden rows are left out entirely: a child in a closed sub-list is not a\n * place the arrow keys can reach, and Home and End land on the first and last\n * row that is actually on screen. The wrapping and disabled-row rules are the\n * same ones the rail uses everywhere else, applied to what is visible.\n */\nexport function rovingRow(\n  key: string,\n  current: number,\n  rows: readonly RailRowState[]\n): number | null {\n  const visible = visibleIndexes(rows);\n  if (visible.length === 0) return null;\n  const next = rovingIndex(\n    key,\n    visible.indexOf(current),\n    visible.map((index) => rows[index]!.disabled)\n  );\n  return next === null ? null : visible[next]!;\n}\n\n/**\n * Where the one pill is drawn.\n *\n * A visible active row wins. When the only active destination is a child in a\n * closed list, its parent holds the pill instead: the reader still sees where\n * they are on the rail rather than losing the marker with the label. The\n * parent is a fallback, not a theft — as soon as the child is drawn it takes\n * the pill back.\n */\nexport function activeRowIndex(rows: readonly RailRowState[]): number | null {\n  const direct = rows.findIndex((row) => row.active && !row.hidden);\n  if (direct >= 0) return direct;\n  const parent = rows.findIndex((row) => row.childActive && !row.hidden);\n  return parent >= 0 ? parent : null;\n}\n\n/** What a movement key asks of a parent row and its child list. */\nexport type SubKeyAction = 'open' | 'enter' | 'close';\n\n/**\n * The action a key takes on a row that owns a child list, or `null`.\n *\n * Right opens a closed list and steps into an open one, Left closes the list a\n * row sits in (and a parent's own list), and Escape closes either. The rail\n * only answers these keys when the row has something for them to act on, so\n * Left on a plain destination is left to the browser.\n */\nexport function subKeyAction(\n  key: string,\n  row: Pick<RailRowState, 'branch' | 'expanded' | 'nested'>\n): SubKeyAction | null {\n  if (key === 'ArrowRight') {\n    if (!row.branch) return null;\n    return row.expanded ? 'enter' : 'open';\n  }\n  if (key === 'ArrowLeft') {\n    if (row.nested) return 'close';\n    if (row.branch && row.expanded) return 'close';\n    return null;\n  }\n  if (key === 'Escape') {\n    return row.nested || row.branch ? 'close' : null;\n  }\n  return null;\n}\n\n/** The text of a node when it is only text, for a name that has to survive collapsing. */\nexport function textOf(node: unknown): string | undefined {\n  if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {\n    const text = String(node).trim();\n    return text.length > 0 ? text : undefined;\n  }\n  if (Array.isArray(node)) {\n    const text = node\n      .map((part) => textOf(part))\n      .filter((part): part is string => part !== undefined)\n      .join(' ')\n      .trim();\n    return text.length > 0 ? text : undefined;\n  }\n  return undefined;\n}\n"
    }
  ]
}
