{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "secondary-nav",
  "title": "Secondary Nav",
  "description": "Secondary Nav from the Get IT Done design system.",
  "dependencies": [],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/progress.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/row-scroll.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/secondary-nav.tsx",
      "content": "'use client';\n\nimport { forwardRef, useEffect, useRef, useState, type AnchorHTMLAttributes, type HTMLAttributes, type KeyboardEvent, type ReactElement, type ReactNode } from 'react';\n\nimport { focusRing, transitionBase } from '@/lib/recipes';\nimport { rowScrollTarget } from '@/lib/row-scroll';\nimport { cn } from '@/lib/utils';\nimport { Progress } from '@/components/ui/progress';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\nexport interface SecondaryNavItem {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n  // Count shown as text at the right, e.g. open items.\n  count?: number;\n  // Second badge after the count, e.g. a small Chip with a status (`Draft`, `Error`). Pass visible text, because colour alone is not enough. Hidden in the collapsed rail like the count.\n  badge?: ReactNode;\n  // Renders a link instead of a button.\n  href?: string;\n  disabled?: boolean;\n}\n\nexport interface SecondaryNavSection {\n  id: string;\n  // Section header. Omit for an unlabelled group.\n  label?: string;\n  items: SecondaryNavItem[];\n}\n\n// Props to spread on the link element returned by `renderLink` (for example a Next.js Link).\nexport type SecondaryNavLinkProps = Pick<AnchorHTMLAttributes<HTMLAnchorElement>, 'className' | 'onClick' | 'title' | 'children'> & {\n  href: string;\n  'aria-current'?: 'page';\n  'aria-disabled'?: true;\n};\n\n// vertical: column (default). horizontal: scrollable single row. responsive: row until the nearest container (an ancestor with the `@container` class) is at the `@area` container screen (860px) wide, then the column.\nexport type SecondaryNavOrientation = 'vertical' | 'horizontal' | 'responsive';\n\nexport interface SecondaryNavProps extends Omit<HTMLAttributes<HTMLElement>, 'onSelect'> {\n  'aria-label': string;\n  sections: SecondaryNavSection[];\n  // Selected item id, controlled.\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (id: string) => void;\n  // sm uses the nav item height, md the small control height.\n  size?: 'sm' | 'md';\n  // Slot below the list, pinned to the bottom (e.g. StorageMeter).\n  footer?: ReactNode;\n  // Custom link element for items with `href`, so client routing keeps working: renderLink={(item, props) => <Link {...props} />}.\n  renderLink?: (item: SecondaryNavItem, props: SecondaryNavLinkProps) => ReactElement;\n  // Icon-only rail. Labels stay available to screen readers and as a title, counts and section headers are hidden. Give every item an icon.\n  collapsed?: boolean;\n  // Layout. `responsive` measures the nearest ancestor with the `@container` class (the page area, not the viewport), as the zero prototype does. In the row, the footer is hidden and `collapsed` is ignored.\n  orientation?: SecondaryNavOrientation;\n}\n\nconst itemSize = { sm: 'h-nav-item', md: 'h-control-sm' } as const;\n\n// Pick a class string by orientation. The strings stay literal so Tailwind sees every class.\nconst pick = (orientation: SecondaryNavOrientation, vertical: string, horizontal: string, responsive: string) =>\n  orientation === 'vertical' ? vertical : orientation === 'horizontal' ? horizontal : responsive;\n\n// Scroller of the horizontal row: one line, padding inside the scroller so the focus ring (2px ring + 2px offset) is never clipped, scroll padding the width of the edge fade so a focused item never sits under it, hidden scrollbar (the fade and the cut-off item show more content).\nconst rowScroller =\n  'relative flex flex-row items-center gap-1 overflow-x-auto px-4 py-2 scroll-px-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ' +\n  // Fade only at an edge that has more content. The width is the space-8 token (32px, as in the zero prototype). Switched off for reduced transparency and more contrast.\n  '[mask-image:linear-gradient(to_right,transparent,#000_var(--fade-start,0px),#000_calc(100%-var(--fade-end,0px)),transparent)] ' +\n  'data-[fade-start]:[--fade-start:var(--space-8)] data-[fade-end]:[--fade-end:var(--space-8)] ' +\n  'contrast-more:[mask-image:none] [@media(prefers-reduced-transparency:reduce)]:[mask-image:none]';\n\n// Row items keep the 30px visual height. Under a coarse pointer (touch) a 44px hit area (touch-target-lg token, WCAG 2.5.5) is added by a centred pseudo element that only grows up and down, so neighbours never overlap. From the `@area` screen the responsive nav is a column again and the hit area is the item itself.\nconst rowHitArea =\n  'relative [@media(pointer:coarse)]:after:absolute [@media(pointer:coarse)]:after:inset-x-0 [@media(pointer:coarse)]:after:top-1/2 [@media(pointer:coarse)]:after:h-[var(--size-touch-target-lg)] [@media(pointer:coarse)]:after:-translate-y-1/2';\nconst rowHitAreaResponsive = cn(rowHitArea, '@area:after:hidden');\n\nconst listItems = (root: HTMLElement | null) =>\n  root ? Array.from(root.querySelectorAll<HTMLElement>('a[href], button')).filter((el) => !(el as HTMLButtonElement).disabled && el.getAttribute('aria-disabled') !== 'true') : [];\n\n// Left edge of an item inside the scroller, measured with bounding boxes (rounded like `offsetLeft`), not `offsetLeft` itself, which depends on the nearest positioned ancestor.\nconst leftIn = (el: HTMLElement, item: HTMLElement) => Math.round(item.getBoundingClientRect().left - el.getBoundingClientRect().left + el.scrollLeft - el.clientLeft);\n\n// The first and last entry of the row scroll fully to the start and the end, so the edge fade on that side goes.\nconst edgeOf = (el: HTMLElement, item: HTMLElement): 'start' | 'end' | undefined => {\n  const all = Array.from(el.querySelectorAll<HTMLElement>('a[href], button'));\n  return item === all[0] ? 'start' : item === all[all.length - 1] ? 'end' : undefined;\n};\n\n// The element that scrolls the column: the nearest ancestor of the item that scrolls vertically, up to the layout (or the page, which is never moved). Usually the nav itself (`overflow-y-auto` on it) or a wrapper the app gives a height.\nconst columnScroller = (item: HTMLElement): HTMLElement | null => {\n  for (let el = item.parentElement; el && el !== document.body && el !== document.documentElement; el = el.parentElement) {\n    const overflowY = getComputedStyle(el).overflowY;\n    if ((overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight) return el;\n    if (el.getAttribute('data-slot') === 'secondary-nav-layout') break;\n  }\n  return null;\n};\n\n// Scroll an item of the column into its scroller (the first and last entry fully to the start and the end). `centre` puts it in the middle (on load), otherwise the smallest move that shows it whole (arrow keys). Only the scroller moves, never the page.\nconst revealInColumn = (item: HTMLElement, mode: 'minimal' | 'centre', animate = false, edge?: 'start' | 'end') => {\n  const el = columnScroller(item);\n  if (!el) return;\n  const box = el.getBoundingClientRect();\n  const rect = item.getBoundingClientRect();\n  const top = Math.round(rect.top - box.top + el.scrollTop - el.clientTop);\n  const bottom = top + item.offsetHeight;\n  let target = el.scrollTop;\n  // The first and last entry go fully to the start and the end of the column, so a section label above the first one is shown too.\n  if (edge) target = edge === 'start' ? 0 : el.scrollHeight;\n  else if (mode === 'centre') {\n    if (top >= el.scrollTop && bottom <= el.scrollTop + el.clientHeight) return;\n    target = top + item.offsetHeight / 2 - el.clientHeight / 2;\n  } else if (top < el.scrollTop) target = top;\n  else if (bottom > el.scrollTop + el.clientHeight) target = bottom - el.clientHeight;\n  target = Math.min(el.scrollHeight - el.clientHeight, Math.max(0, Math.round(target)));\n  if (target === el.scrollTop) return;\n  const reduced = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n  if (animate && !reduced && typeof el.scrollTo === 'function') el.scrollTo({ top: target, behavior: 'smooth' });\n  else el.scrollTop = target;\n};\n\n// Navigation inside a page (settings, file spaces). A nav landmark with aria-current, not a tablist. Vertical column by default, a scrollable single row with `horizontal`, and `responsive` switches from the row to the column when the nearest container is 860px wide or more.\nconst SecondaryNav = forwardRef<HTMLElement, SecondaryNavProps>(\n  ({ className, sections, value, defaultValue, onValueChange, size = 'sm', footer, renderLink, collapsed: collapsedProp = false, orientation = 'vertical', onKeyDown, ...props }, ref) => {\n    const [inner, setInner] = useState(defaultValue);\n    // Zero: 8px item padding and gap, 2px list gap, subtle icons, 11px group label.\n    const zero = usePreset() === 'zero';\n    const current = value ?? inner;\n    const o = orientation;\n    // The icon rail only exists in the vertical column.\n    const collapsed = collapsedProp && o === 'vertical';\n    const scrollerRef = useRef<HTMLDivElement>(null);\n    const [fade, setFade] = useState({ start: false, end: false });\n    const select = (id: string) => {\n      if (value === undefined) setInner(id);\n      onValueChange?.(id);\n    };\n\n    // Edge fades only while the row overflows and only on the side that has more content.\n    useEffect(() => {\n      const el = scrollerRef.current;\n      if (!el || o === 'vertical') return undefined;\n      const measure = () => {\n        const overflow = el.scrollWidth - el.clientWidth > 1;\n        const start = overflow && el.scrollLeft > 1;\n        const end = overflow && el.scrollLeft + el.clientWidth < el.scrollWidth - 1;\n        setFade((prev) => (prev.start === start && prev.end === end ? prev : { start, end }));\n      };\n      measure();\n      el.addEventListener('scroll', measure, { passive: true });\n      const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measure);\n      observer?.observe(el);\n      return () => {\n        el.removeEventListener('scroll', measure);\n        observer?.disconnect();\n      };\n    }, [o, sections]);\n\n    // Scroll the row so an item is fully visible and clear of the edge fade. Only the scroller moves, never the page.\n    const reveal = (item: HTMLElement) => {\n      const el = scrollerRef.current;\n      if (!el || el.scrollWidth <= el.clientWidth) return;\n      const pad = parseFloat(getComputedStyle(el).scrollPaddingLeft) || 0;\n      const target = rowScrollTarget(el, { left: leftIn(el, item), width: item.offsetWidth, edge: edgeOf(el, item) }, pad, false);\n      if (target !== el.scrollLeft) el.scrollLeft = target;\n    };\n\n    // Centre the current item in the row. Only the scroller moves, never the page (no scrollIntoView, which would scroll every ancestor). On mount it jumps, so the page loads with the section in place. When the current item changes later it glides, unless the user prefers reduced motion. An item near an end cannot be centred and stops at the end.\n    const centre = (item: HTMLElement, animate: boolean) => {\n      const el = scrollerRef.current;\n      if (!el || el.scrollWidth <= el.clientWidth) return;\n      const pad = parseFloat(getComputedStyle(el).scrollPaddingLeft) || 0;\n      const target = rowScrollTarget(el, { left: leftIn(el, item), width: item.offsetWidth, edge: edgeOf(el, item) }, pad, true);\n      if (target === el.scrollLeft) return;\n      const reduced = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n      if (animate && !reduced && typeof el.scrollTo === 'function') el.scrollTo({ left: target, behavior: 'smooth' });\n      else el.scrollLeft = target;\n    };\n    // Item ids as one string: the centring effect runs when the items change, not on every render with a new `sections` array (which would pull the row back while the user scrolls it).\n    const itemKey = sections.flatMap((section) => section.items.map((item) => item.id)).join('|');\n    const mounted = useRef(false);\n    // Set once the user scrolls or presses in the row, so a late re-centre (web fonts) never fights them.\n    const touched = useRef(false);\n\n    // Bring the current item to the middle of the scroller (on mount and when it changes).\n    useEffect(() => {\n      const el = scrollerRef.current;\n      if (!el || o === 'vertical') return;\n      const item = el.querySelector<HTMLElement>('[aria-current=\"page\"]');\n      if (item) centre(item, mounted.current);\n      mounted.current = true;\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [current, o, itemKey]);\n\n    // Column: bring the current item into view when it sits outside the visible part of a scrolling column (on load and when it changes). Centred, instant on load, gliding later unless reduced motion. The page never moves. The layout is not final on the first pass (container query, heights of the page frame, web fonts), so it is placed again whenever the nav or the layout around it changes size and once fonts are ready, until the user has moved the column.\n    const columnMounted = useRef(false);\n    useEffect(() => {\n      const el = scrollerRef.current;\n      if (!el) return undefined;\n      const place = (animate: boolean) => {\n        const column = o === 'vertical' || (o === 'responsive' && getComputedStyle(el).flexDirection === 'column');\n        const item = el.querySelector<HTMLElement>('[aria-current=\"page\"]');\n        if (column && item) revealInColumn(item, 'centre', animate);\n      };\n      place(columnMounted.current);\n      columnMounted.current = true;\n      let active = true;\n      const settle = () => {\n        if (active && !touched.current) place(false);\n      };\n      if (typeof document !== 'undefined' && document.fonts?.ready) document.fonts.ready.then(settle);\n      const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(settle);\n      [el.parentElement, el.closest('[data-slot=\"secondary-nav-layout\"]')].forEach((target) => target && observer?.observe(target));\n      return () => {\n        active = false;\n        observer?.disconnect();\n      };\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [current, o, itemKey]);\n\n    // Web fonts, a badge or a count that arrives late change the item widths after the first paint, so the centre is measured again once fonts are ready and whenever an item changes size (unless the user already moved the row).\n    useEffect(() => {\n      const el = scrollerRef.current;\n      if (!el || o === 'vertical') return undefined;\n      let active = true;\n      const settle = () => {\n        if (!active || touched.current) return;\n        const item = el.querySelector<HTMLElement>('[aria-current=\"page\"]');\n        if (item) centre(item, false);\n      };\n      if (typeof document !== 'undefined' && document.fonts?.ready) document.fonts.ready.then(settle);\n      const sizes = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(settle);\n      el.querySelectorAll('a[href], button').forEach((item) => sizes?.observe(item));\n      return () => {\n        active = false;\n        sizes?.disconnect();\n      };\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [o, itemKey]);\n\n    // Arrow keys, Home and End move focus between the enabled items. Left and Right in the row, Up and Down where the list renders as a column (`responsive` from the `@area` screen). Tab order is unchanged. The focused item is revealed inside the scroller, so a partly hidden item is never left under the edge fade.\n    const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n      onKeyDown?.(event);\n      touched.current = true;\n      if (event.defaultPrevented || o === 'vertical' || event.altKey || event.ctrlKey || event.metaKey) return;\n      const scroller = scrollerRef.current;\n      const items = listItems(scroller);\n      const index = items.indexOf(event.target as HTMLElement);\n      if (index < 0 || !scroller) return;\n      const style = getComputedStyle(scroller);\n      const column = o === 'responsive' && style.flexDirection === 'column';\n      const rtl = style.direction === 'rtl';\n      const forward = column ? 'ArrowDown' : rtl ? 'ArrowLeft' : 'ArrowRight';\n      const backward = column ? 'ArrowUp' : rtl ? 'ArrowRight' : 'ArrowLeft';\n      let next = -1;\n      if (event.key === forward) next = index + 1;\n      else if (event.key === backward) next = index - 1;\n      else if (event.key === 'Home') next = 0;\n      else if (event.key === 'End') next = items.length - 1;\n      else return;\n      event.preventDefault();\n      const target = items[Math.min(items.length - 1, Math.max(0, next))];\n      if (!target) return;\n      target.focus({ preventScroll: true });\n      if (column) revealInColumn(target, 'minimal', false, edgeOf(scroller, target));\n      else reveal(target);\n    };\n\n    return (\n      <nav\n        ref={ref}\n        data-collapsed={collapsed || undefined}\n        data-orientation={o}\n        onKeyDown={handleKeyDown}\n        className={cn(\n          'typography-ui flex max-w-full flex-col text-foreground',\n          pick(o, cn('justify-between gap-4', collapsed ? 'w-sidebar-collapsed' : 'w-inner-column'), 'w-full', 'w-full @area:w-inner-column @area:justify-between @area:gap-4'),\n          className,\n        )}\n        {...props}\n      >\n        <div\n          ref={scrollerRef}\n          onWheel={() => {\n            touched.current = true;\n          }}\n          onTouchStart={() => {\n            touched.current = true;\n          }}\n          onPointerDown={() => {\n            touched.current = true;\n          }}\n          data-fade-start={fade.start || undefined}\n          data-fade-end={fade.end || undefined}\n          className={pick(\n            o,\n            'flex flex-col gap-4',\n            rowScroller,\n            cn(rowScroller, '@area:flex-col @area:items-stretch @area:gap-4 @area:overflow-visible @area:p-0 @area:[mask-image:none]'),\n          )}\n        >\n          {sections.map((section) => (\n            <div\n              key={section.id}\n              className={pick(\n                o,\n                'flex flex-col gap-1',\n                'ml-2 flex shrink-0 flex-row items-center gap-2 border-l border-border pl-3 first:ml-0 first:border-l-0 first:pl-0',\n                'ml-2 flex shrink-0 flex-row items-center gap-2 border-l border-border pl-3 first:ml-0 first:border-l-0 first:pl-0 @area:ml-0 @area:shrink @area:flex-col @area:items-stretch @area:gap-1 @area:border-l-0 @area:pl-0',\n              )}\n            >\n              {section.label && !collapsed ? (\n                <p\n                  className={cn(\n                    'text-muted-foreground',\n                    zero ? 'typography-kpi-label' : 'typography-text-extra-small',\n                    pick(o, zero ? 'px-2' : 'px-3', 'shrink-0 whitespace-nowrap', cn('shrink-0 whitespace-nowrap', zero ? '@area:px-2' : '@area:px-3')),\n                  )}\n                >\n                  {section.label}\n                </p>\n              ) : null}\n              <ul\n                className={cn(\n                  'm-0 flex list-none p-0',\n                  pick(o, cn('flex-col', zero ? 'gap-micro' : 'gap-1'), 'flex-row items-center gap-1', cn('flex-row items-center gap-1', zero ? '@area:flex-col @area:items-stretch @area:gap-micro' : '@area:flex-col @area:items-stretch')),\n                )}\n              >\n                {section.items.map((item) => {\n                  const selected = current === item.id;\n                  const cls = cn(\n                    'typography-ui flex items-center rounded-item text-left',\n                    pick(o, 'w-full', cn('shrink-0 whitespace-nowrap', rowHitArea), cn('shrink-0 whitespace-nowrap @area:w-full', rowHitAreaResponsive)),\n                    zero ? 'gap-2 px-2' : 'gap-3 px-3',\n                    collapsed && 'justify-center px-0',\n                    itemSize[size],\n                    selected ? 'bg-muted font-medium text-foreground' : cn(zero ? 'text-sidebar-foreground' : 'text-muted-foreground', 'hover:bg-muted hover:text-foreground'),\n                    item.disabled && 'pointer-events-none opacity-disabled',\n                    transitionBase,\n                    focusRing,\n                  );\n                  const body = (\n                    <>\n                      {item.icon ? (\n                        <span aria-hidden=\"true\" className={cn('flex h-icon-nav w-icon-nav shrink-0 items-center justify-center [&>svg]:h-icon-nav [&>svg]:w-icon-nav', zero && !selected && 'text-icon-subtle')}>\n                          {item.icon}\n                        </span>\n                      ) : null}\n                      <span className={cn(pick(o, 'min-w-0 flex-1 truncate', 'whitespace-nowrap', 'whitespace-nowrap @area:min-w-0 @area:flex-1 @area:truncate'), collapsed && 'sr-only')}>{item.label}</span>\n                      {item.count != null && !collapsed ? <span className=\"typography-text-extra-small tabular-nums text-muted-foreground\">{item.count}</span> : null}\n                      {item.badge != null && !collapsed ? (\n                        <span data-nav-badge=\"\" className=\"flex shrink-0 items-center\">\n                          {item.badge}\n                        </span>\n                      ) : null}\n                    </>\n                  );\n                  return (\n                    <li key={item.id} className={pick(o, '', 'shrink-0', 'shrink-0 @area:shrink') || undefined}>\n                      {item.href && renderLink ? (\n                        renderLink(item, {\n                          href: item.href,\n                          className: cls,\n                          title: collapsed ? item.label : undefined,\n                          'aria-current': selected ? 'page' : undefined,\n                          'aria-disabled': item.disabled ? true : undefined,\n                          onClick: () => select(item.id),\n                          children: body,\n                        })\n                      ) : item.href ? (\n                        <a\n                          href={item.href}\n                          title={collapsed ? item.label : undefined}\n                          aria-current={selected ? 'page' : undefined}\n                          aria-disabled={item.disabled || undefined}\n                          onClick={() => select(item.id)}\n                          className={cls}\n                        >\n                          {body}\n                        </a>\n                      ) : (\n                        <button type=\"button\" title={collapsed ? item.label : undefined} aria-current={selected ? 'page' : undefined} disabled={item.disabled} onClick={() => select(item.id)} className={cls}>\n                          {body}\n                        </button>\n                      )}\n                    </li>\n                  );\n                })}\n              </ul>\n            </div>\n          ))}\n        </div>\n        {footer && !collapsed && o !== 'horizontal' ? <div className={pick(o, 'px-3', '', 'hidden px-3 @area:block')}>{footer}</div> : null}\n      </nav>\n    );\n  },\n);\nSecondaryNav.displayName = 'SecondaryNav';\n\nexport interface SecondaryNavLayoutProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> {\n  // The SecondaryNav (use `orientation=\"responsive\"`). It is placed above the content in a narrow area and as a left column from the `@area` screen.\n  nav: ReactNode;\n  children: ReactNode;\n  // Makes this wrapper the size container the nav measures. Turn off when an ancestor with the `@container` class already is the page area.\n  container?: boolean;\n  // Takes the height the parent offers: the layout, its row and the content slot get `h-full min-h-0 flex-1`, so a scrolling content area works without `[&>div]:h-full` overrides. The parent needs a definite height or to be a flex column.\n  fill?: boolean;\n  // Area title in the column header above the nav. Shown from the `@area` screen only: in the narrow row the nav carries navigation and nothing else, so the page header names the area. Pass visible text (a heading element if the page outline needs one).\n  title?: ReactNode;\n  // Actions of the area (for example an icon button): right of the title in the column header from the `@area` screen, and below it a fixed field at the end of the narrow row, outside the scrolling nav, so the action is always reachable. Use icon buttons with an `aria-label`; under a coarse pointer their hit area is 44px.\n  actions?: ReactNode;\n  // Extra classes for the nav slot and the content slot. The slot is the outer element: the title/actions grid is an inner element, so `flex flex-col` or similar on `navClassName` cannot replace it.\n  navClassName?: string;\n  contentClassName?: string;\n}\n\n// Page frame for a SecondaryNav: nav above the content in a narrow area (hairline below), left column with a hairline on its end from the `@area` screen. Replaces the `flex-col @area:flex-row` plus width and border classes apps wrote around the nav.\nconst SecondaryNavLayout = forwardRef<HTMLDivElement, SecondaryNavLayoutProps>(({ className, nav, children, container = true, fill = false, title, actions, navClassName, contentClassName, ...props }, ref) => {\n  const zero = usePreset() === 'zero';\n  const header = title != null || actions != null;\n  return (\n  <div\n    ref={ref}\n    data-slot=\"secondary-nav-layout\"\n    data-fill={fill || undefined}\n    className={cn(container && '@container', 'w-full', fill && 'flex h-full min-h-0 flex-1 flex-col', className)}\n    {...props}\n  >\n    <div className={cn('flex flex-col @area:flex-row', fill && 'h-full min-h-0 flex-1')}>\n      <div\n        data-slot=\"secondary-nav-slot\"\n        className={cn('shrink-0 border-b border-solid border-border @area:border-b-0 @area:border-r', header && '@area:flex @area:min-h-0 @area:flex-col', navClassName)}\n      >\n        {/* With a header the grid lives on an inner element, so an app `navClassName` such as `flex flex-col` on the slot can never replace it in class merging. Below `@area`: the actions (fixed field at the start, outside the scrolling nav) and the nav share one row. From `@area`: title and actions form the header row above the nav, and the second row takes the height left over (`minmax(0,1fr)`), so a scrolling column nav fills the column. Stretching, no centring in the column. One DOM node each, in visual order (title, actions, nav), so the tab order always follows the picture. */}\n        {header ? (\n          <div data-slot=\"secondary-nav-grid\" className=\"grid grid-cols-[auto_minmax(0,1fr)] items-center @area:h-full @area:min-h-0 @area:grid-cols-[minmax(0,1fr)_auto] @area:grid-rows-[auto_minmax(0,1fr)] @area:items-stretch\">\n            {title != null ? (\n              <div data-slot=\"secondary-nav-title\" className={cn('typography-title-sm col-start-2 row-start-1 hidden min-w-0 truncate py-3 text-foreground @area:col-start-1 @area:block', zero ? 'px-2' : 'px-3')}>\n                {title}\n              </div>\n            ) : null}\n            {actions != null ? (\n              <div\n                data-slot=\"secondary-nav-actions\"\n                className={cn(\n                  'col-start-1 row-start-1 flex shrink-0 items-center gap-1 self-stretch border-0 border-r border-solid border-border px-2 @area:col-start-2 @area:border-r-0 @area:py-3',\n                  zero ? '@area:px-2' : '@area:px-3',\n                  // Touch: every icon button in the field has a 44px hit area (touch-target-lg, WCAG 2.5.5).\n                  '[@media(pointer:coarse)]:[&_button]:min-h-[var(--size-touch-target-lg)] [@media(pointer:coarse)]:[&_button]:min-w-[var(--size-touch-target-lg)]',\n                )}\n              >\n                {actions}\n              </div>\n            ) : null}\n            <div data-slot=\"secondary-nav-nav\" className=\"col-start-2 row-start-1 flex min-h-0 min-w-0 flex-col @area:col-span-2 @area:col-start-1 @area:row-start-2 [&>*]:min-h-0 @area:[&>*]:flex-1\">\n              {nav}\n            </div>\n          </div>\n        ) : (\n          nav\n        )}\n      </div>\n      <div data-slot=\"secondary-nav-content\" className={cn('min-w-0 flex-1', fill && 'min-h-0', contentClassName)}>\n        {children}\n      </div>\n    </div>\n  </div>\n  );\n});\nSecondaryNavLayout.displayName = 'SecondaryNavLayout';\n\nexport interface StorageMeterProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n  used: number;\n  total: number;\n  label?: string;\n  // Unit appended to the numbers, e.g. \"GB\".\n  unit?: string;\n}\n\n// Used and total storage with a progress bar. The text carries the values, the bar is the visual.\nconst StorageMeter = forwardRef<HTMLDivElement, StorageMeterProps>(({ className, used, total, label: labelProp, unit = 'GB', ...props }, ref) => {\n  const l = useLabels();\n  const label = labelProp ?? l.storage;\n  const percent = total > 0 ? Math.round((used / total) * 100) : 0;\n  const near = percent >= 90;\n  return (\n    <div ref={ref} className={cn('flex flex-col gap-2', className)} {...props}>\n      <p className=\"typography-text-extra-small flex items-baseline justify-between gap-2 text-muted-foreground\">\n        <span>{label}</span>\n        <span className={cn('tabular-nums', near && 'text-text-destructive')}>\n          {l.storageUsed(used, total, unit)}\n        </span>\n      </p>\n      <Progress size=\"sm\" value={used} max={total} variant={near ? 'destructive' : 'default'} aria-label={l.storagePercentUsed(label, percent)} />\n    </div>\n  );\n});\nStorageMeter.displayName = 'StorageMeter';\n\nexport { SecondaryNav, SecondaryNavLayout, StorageMeter };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}