{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "dependencies": [],
  "description": "ARIA APG tabs with nullable controlled or uncontrolled selection, one roving tab stop, horizontal or vertical navigation, automatic or manual activation, disabled-tab skipping, explicit external-panel wiring, and safe generated ids. Composition via native host props and renderHeidiElement.",
  "files": [
    {
      "content": "\"use client\";\n\n/**\n * heidi-ui Tabs — the ARIA APG tabs pattern: role=tablist/tab/tabpanel,\n * roving tabindex, horizontal/vertical arrow navigation, optional manual\n * activation, disabled-tab skipping, and controlled/uncontrolled selection.\n * A null selection still leaves the first enabled tab as the one tab stop.\n * The remembered roving stop survives only while focus is inside the tablist;\n * tabbing back in always lands on the selected tab, per the APG.\n *\n * Tab and Panel ids default to an instance-safe encoding of their value.\n * Consumers may supply explicit `id`, `aria-controls`, and `aria-labelledby`\n * when one shared or externally placed panel owns the rendered content.\n *\n * Composition: native host props + className / style / render / ref via\n * renderHeidiElement. Structural CSS: tabs.base.css. Theme: tabs.theme.css.\n */\n\nimport {\n  Children,\n  type ComponentPropsWithoutRef,\n  createContext,\n  Fragment,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type FocusEvent,\n  type KeyboardEvent,\n  type MouseEvent,\n  type ReactNode,\n  type Ref\n} from \"react\";\nimport {\n  collectRovingItems,\n  NAVIGATION_KEYS,\n  resolveRovingIndex\n} from \"../_internal/roving-focus\";\nimport { isDisabledElement, nativeDisabledAttrs } from \"../_internal/disabled\";\nimport {\n  composeHeidiEventHandlers,\n  mergeHeidiRefs,\n  renderHeidiElement,\n  type HeidiIntrinsicHostProps\n} from \"../_internal/render-element\";\nimport { safeId } from \"../_internal/safe-id\";\nimport { useHeidiLayoutEffect } from \"../_internal/use-layout-effect\";\nimport { type TabsOrientation } from \"./tabs.anatomy.generated\";\nimport { TABS_CLASSES } from \"./tabs.classes.generated\";\n\nexport type { TabsOrientation };\nexport type TabsActivationMode = \"automatic\" | \"manual\";\n\ntype TabsEntry = {\n  disabled: boolean;\n  id: string;\n  value: string;\n};\n\ntype TabsPanelEntry = {\n  id: string;\n  value: string;\n};\n\ntype TabsContextValue = {\n  activationMode: TabsActivationMode;\n  idPrefix: string;\n  orientation: TabsOrientation;\n  panelIdForValue: (value: string) => string;\n  registerPanel: (entry: TabsPanelEntry) => void;\n  registerTab: (entry: TabsEntry) => void;\n  select: (value: string) => void;\n  selected: string | null;\n  /** `null` forgets the remembered stop, reverting it to the selected tab. */\n  setTabbableId: (id: string | null) => void;\n  syncTabOrder: (entries: TabsEntry[]) => void;\n  tabIdForValue: (value: string) => string;\n  tabbableId: string | null;\n  unregisterPanel: (id: string) => void;\n  unregisterTab: (id: string) => void;\n};\n\nconst TabsContext = createContext<TabsContextValue | null>(null);\nconst TabsListInitialTabStopContext = createContext<string | null>(null);\n\nfunction useTabsContext(part: string): TabsContextValue {\n  const context = useContext(TabsContext);\n  if (!context) {\n    throw new Error(`Tabs.${part} must be rendered inside Tabs.Root.`);\n  }\n  return context;\n}\n\n/** Lossless for Unicode scalar values and safe to place directly in a DOM id. */\nfunction encodeValueForId(value: string): string {\n  if (value === \"\") {\n    return \"empty\";\n  }\n  return Array.from(value, (character) => character.codePointAt(0)?.toString(36) ?? \"0\").join(\"-\");\n}\n\n/** State handed to a `TabsRoot` className/style/render callback. */\nexport type TabsRootState = {\n  activationMode: TabsActivationMode;\n  orientation: TabsOrientation;\n  value: string | null;\n};\n\ntype TabsRootNativeProps = Omit<\n  ComponentPropsWithoutRef<\"div\">,\n  \"children\" | \"className\" | \"defaultValue\" | \"ref\" | \"style\"\n>;\n\nexport type TabsRootProps = HeidiIntrinsicHostProps<TabsRootState, \"div\"> &\n  TabsRootNativeProps & {\n    activationMode?: TabsActivationMode;\n    children?: ReactNode;\n    defaultValue?: string | null;\n    onValueChange?: (value: string) => void;\n    orientation?: TabsOrientation;\n    ref?: Ref<HTMLDivElement>;\n    /** Controlled selection. `null` intentionally leaves no tab selected. */\n    value?: string | null;\n  };\n\nexport function TabsRoot({\n  activationMode = \"automatic\",\n  children,\n  className,\n  defaultValue = null,\n  onValueChange,\n  orientation = \"horizontal\",\n  ref,\n  render,\n  style,\n  value: valueProp,\n  ...nativeProps\n}: TabsRootProps) {\n  const generatedId = safeId(useId());\n  const idPrefix = `hui-tabs-${generatedId}`;\n  const [internal, setInternal] = useState<string | null>(defaultValue);\n  const [panels, setPanels] = useState<TabsPanelEntry[]>([]);\n  const [rovingTabId, setRovingTabId] = useState<string | null>(null);\n  const [tabs, setTabs] = useState<TabsEntry[]>([]);\n  const controlled = valueProp !== undefined;\n  const selected = controlled ? valueProp : internal;\n  // ponytail: Registration effects cannot pair explicit Tab/Panel ids in SSR\n  // markup. Read transparent authored wrappers first; effects remain the\n  // fallback for opaque custom components after hydration.\n  const preparedEntries = useMemo(() => collectPreparedEntries(children, idPrefix), [children, idPrefix]);\n\n  const select = useCallback(\n    (next: string) => {\n      if (!controlled) {\n        setInternal(next);\n      }\n      onValueChange?.(next);\n    },\n    [controlled, onValueChange]\n  );\n\n  const registerTab = useCallback((entry: TabsEntry) => {\n    setTabs((previous) => {\n      const current = previous.find((candidate) => candidate.id === entry.id);\n      if (current?.disabled === entry.disabled && current.value === entry.value) {\n        return previous;\n      }\n      if (current) {\n        return previous.map((candidate) => (candidate.id === entry.id ? entry : candidate));\n      }\n      return [...previous, entry];\n    });\n  }, []);\n\n  const unregisterTab = useCallback((id: string) => {\n    setTabs((previous) => previous.filter((entry) => entry.id !== id));\n  }, []);\n\n  const syncTabOrder = useCallback((entries: TabsEntry[]) => {\n    setTabs((previous) => {\n      if (\n        previous.length === entries.length &&\n        previous.every(\n          (entry, index) =>\n            entry.disabled === entries[index]?.disabled &&\n            entry.id === entries[index]?.id &&\n            entry.value === entries[index]?.value\n        )\n      ) {\n        return previous;\n      }\n      return entries;\n    });\n  }, []);\n\n  const registerPanel = useCallback((entry: TabsPanelEntry) => {\n    setPanels((previous) => {\n      const current = previous.find((candidate) => candidate.id === entry.id);\n      if (current?.value === entry.value) {\n        return previous;\n      }\n      if (current) {\n        return previous.map((candidate) => (candidate.id === entry.id ? entry : candidate));\n      }\n      return [...previous, entry];\n    });\n  }, []);\n\n  const unregisterPanel = useCallback((id: string) => {\n    setPanels((previous) => previous.filter((entry) => entry.id !== id));\n  }, []);\n\n  const generatedTabId = useCallback((value: string) => `${idPrefix}-tab-${encodeValueForId(value)}`, [idPrefix]);\n  const generatedPanelId = useCallback((value: string) => `${idPrefix}-panel-${encodeValueForId(value)}`, [idPrefix]);\n  const tabIdForValue = useCallback(\n    (value: string) =>\n      preparedEntries.tabs.find((entry) => entry.value === value)?.id ??\n      tabs.find((entry) => entry.value === value)?.id ??\n      generatedTabId(value),\n    [generatedTabId, preparedEntries.tabs, tabs]\n  );\n  const panelIdForValue = useCallback(\n    (value: string) =>\n      preparedEntries.panels.find((entry) => entry.value === value)?.id ??\n      panels.find((entry) => entry.value === value)?.id ??\n      generatedPanelId(value),\n    [generatedPanelId, panels, preparedEntries.panels]\n  );\n\n  const enabledTabs = tabs.filter((entry) => !entry.disabled);\n  const tabbableId =\n    enabledTabs.find((entry) => entry.id === rovingTabId)?.id ??\n    enabledTabs.find((entry) => entry.value === selected)?.id ??\n    enabledTabs[0]?.id ??\n    null;\n  const setTabbableId = useCallback((id: string | null) => {\n    setRovingTabId(id);\n  }, []);\n\n  const context = useMemo(\n    () => ({\n      activationMode,\n      idPrefix,\n      orientation,\n      panelIdForValue,\n      registerPanel,\n      registerTab,\n      select,\n      selected,\n      setTabbableId,\n      syncTabOrder,\n      tabIdForValue,\n      tabbableId,\n      unregisterPanel,\n      unregisterTab\n    }),\n    [\n      activationMode,\n      idPrefix,\n      orientation,\n      panelIdForValue,\n      registerPanel,\n      registerTab,\n      select,\n      selected,\n      setTabbableId,\n      syncTabOrder,\n      tabIdForValue,\n      tabbableId,\n      unregisterPanel,\n      unregisterTab\n    ]\n  );\n\n  return (\n    <TabsContext value={context}>\n      {renderHeidiElement({\n        className: TABS_CLASSES.root,\n        dataPart: \"tabs-root\",\n        element: \"div\",\n        props: {\n          ...nativeProps,\n          children,\n          \"data-orientation\": orientation,\n          ref\n        },\n        renderProps: { className, render, style },\n        state: { activationMode, orientation, value: selected }\n      })}\n    </TabsContext>\n  );\n}\n\nfunction enabledTabElements(container: HTMLElement): HTMLButtonElement[] {\n  return collectRovingItems<HTMLButtonElement>(\n    container,\n    '[data-hui-part=\"tabs-tab\"]',\n    '[data-hui-part=\"tabs-list\"]',\n    isDisabledElement\n  );\n}\n\nfunction tabEntriesInDomOrder(container: HTMLElement): TabsEntry[] {\n  return [...container.querySelectorAll<HTMLButtonElement>('[data-hui-part=\"tabs-tab\"]')]\n    .filter((tab) => tab.closest('[data-hui-part=\"tabs-list\"]') === container)\n    .map((tab) => ({\n      disabled: isDisabledElement(tab),\n      id: tab.id,\n      value: tab.dataset.value ?? \"\"\n    }));\n}\n\ntype TabCandidateProps = {\n  children?: ReactNode;\n  disabled?: boolean;\n  id?: string;\n  value?: string;\n};\n\ntype PanelCandidateProps = {\n  children?: ReactNode;\n  id?: string;\n  value?: string;\n};\n\ntype PreparedEntries = {\n  panels: TabsPanelEntry[];\n  tabs: TabsEntry[];\n};\n\nfunction collectPreparedEntries(children: ReactNode, idPrefix: string): PreparedEntries {\n  const panels: TabsPanelEntry[] = [];\n  const tabs: TabsEntry[] = [];\n\n  const visit = (nodes: ReactNode) => {\n    Children.forEach(nodes, (child) => {\n      if (!isValidElement<TabCandidateProps | PanelCandidateProps>(child)) {\n        return;\n      }\n      if (child.type === TabsTab) {\n        const props = child.props as TabCandidateProps;\n        if (props.value === undefined) {\n          return;\n        }\n        tabs.push({\n          disabled: props.disabled === true,\n          id: props.id ?? `${idPrefix}-tab-${encodeValueForId(props.value)}`,\n          value: props.value\n        });\n        return;\n      }\n      if (child.type === TabsPanel) {\n        const props = child.props as PanelCandidateProps;\n        if (props.value === undefined) {\n          return;\n        }\n        panels.push({\n          id: props.id ?? `${idPrefix}-panel-${encodeValueForId(props.value)}`,\n          value: props.value\n        });\n        return;\n      }\n      if (child.type === TabsList || child.type === Fragment || typeof child.type === \"string\") {\n        visit(child.props.children);\n      }\n    });\n  };\n\n  visit(children);\n  return { panels, tabs };\n}\n\nfunction initialTabStopIdForChildren(children: ReactNode, idPrefix: string, selected: string | null): string | null {\n  let firstEnabledId: string | null = null;\n  let selectedEnabledId: string | null = null;\n\n  const visit = (nodes: ReactNode) => {\n    Children.forEach(nodes, (child) => {\n      if (!isValidElement<TabCandidateProps>(child)) {\n        return;\n      }\n      if (child.type === TabsTab && child.props.disabled !== true && child.props.value !== undefined) {\n        const id = child.props.id ?? `${idPrefix}-tab-${encodeValueForId(child.props.value)}`;\n        firstEnabledId ??= id;\n        if (child.props.value === selected) {\n          selectedEnabledId = id;\n        }\n        return;\n      }\n      if (child.type === Fragment || typeof child.type === \"string\") {\n        visit(child.props.children);\n      }\n    });\n  };\n\n  visit(children);\n  return selectedEnabledId ?? firstEnabledId;\n}\n\n/** State handed to a `TabsList` className/style/render callback. */\nexport type TabsListState = { orientation: TabsOrientation };\n\ntype TabsListNativeProps = Omit<\n  ComponentPropsWithoutRef<\"div\">,\n  | \"aria-label\"\n  | \"aria-orientation\"\n  | \"children\"\n  | \"className\"\n  | \"onBlur\"\n  | \"onKeyDown\"\n  | \"ref\"\n  | \"role\"\n  | \"style\"\n>;\n\nexport type TabsListProps = HeidiIntrinsicHostProps<TabsListState, \"div\"> &\n  TabsListNativeProps & {\n    children?: ReactNode;\n    label: string;\n    onBlur?: ComponentPropsWithoutRef<\"div\">[\"onBlur\"];\n    onKeyDown?: ComponentPropsWithoutRef<\"div\">[\"onKeyDown\"];\n    ref?: Ref<HTMLDivElement>;\n  };\n\nexport function TabsList({\n  children,\n  className,\n  label,\n  onBlur,\n  onKeyDown,\n  ref,\n  render,\n  style,\n  tabIndex = -1,\n  ...nativeProps\n}: TabsListProps) {\n  const {\n    activationMode,\n    idPrefix,\n    orientation,\n    select,\n    selected,\n    setTabbableId,\n    syncTabOrder\n  } = useTabsContext(\"List\");\n  const initialTabStopId = initialTabStopIdForChildren(children, idPrefix, selected);\n  const listRef = useRef<HTMLDivElement>(null);\n\n  // Keyed React children can move without remounting, so registration effects\n  // alone cannot reveal their current order. Reconcile from the committed DOM\n  // before paint; keyboard navigation reads this same order directly.\n  useHeidiLayoutEffect(() => {\n    const list = listRef.current;\n    if (list) {\n      syncTabOrder(tabEntriesInDomOrder(list));\n    }\n  });\n\n  // ponytail: APG Tabs, Keyboard Interaction — \"When focus moves into the tab\n  // list, places focus on the active tab element.\" The remembered roving stop\n  // is only authoritative WHILE focus is inside the list; once focus leaves,\n  // the single tab stop must revert to the selected tab. Without this, Tab\n  // lands on an aria-selected=\"false\" tab, and in automatic mode the next\n  // ArrowRight advances from there and selects a tab the user never navigated\n  // to. Rejected: clearing the roving id from an effect on `selected` — in\n  // manual activation mode that yanks the tab stop back to the selected tab\n  // while the user is still arrowing through unactivated tabs, and it cannot\n  // express \"stale only because focus left\".\n  const handleBlur = composeHeidiEventHandlers(onBlur, (event: FocusEvent<HTMLDivElement>) => {\n    if (!event.currentTarget.contains(event.relatedTarget)) {\n      setTabbableId(null);\n    }\n  });\n\n  const handleKeyDown = composeHeidiEventHandlers(onKeyDown, (event: KeyboardEvent<HTMLDivElement>) => {\n    if (!NAVIGATION_KEYS.includes(event.key)) {\n      return;\n    }\n    const tabs = enabledTabElements(event.currentTarget);\n    // crossAxis: false — a tablist has ONE axis, and a cross-axis arrow stays\n    // available to the page rather than being swallowed here.\n    const nextIndex = resolveRovingIndex(\n      tabs,\n      document.activeElement,\n      event.key,\n      event.currentTarget,\n      { crossAxis: false, orientation }\n    );\n    if (nextIndex === null) {\n      return;\n    }\n    event.preventDefault();\n    const nextTab = tabs[nextIndex];\n    if (nextTab) {\n      setTabbableId(nextTab.id);\n    }\n    nextTab?.focus();\n    const nextValue = nextTab?.dataset.value;\n    if (activationMode === \"automatic\" && nextValue !== undefined) {\n      select(nextValue);\n    }\n  });\n\n  return (\n    <TabsListInitialTabStopContext value={initialTabStopId}>\n      {renderHeidiElement({\n        className: TABS_CLASSES.list,\n        dataPart: \"tabs-list\",\n        element: \"div\",\n        props: {\n          ...nativeProps,\n          \"aria-label\": label,\n          \"aria-orientation\": orientation,\n          children,\n          \"data-orientation\": orientation,\n          onBlur: handleBlur,\n          onKeyDown: handleKeyDown,\n          ref: mergeHeidiRefs(listRef, ref),\n          role: \"tablist\",\n          tabIndex\n        },\n        renderProps: { className, render, style },\n        state: { orientation }\n      })}\n    </TabsListInitialTabStopContext>\n  );\n}\n\n/** State handed to a `TabsTab` className/style/render callback. */\nexport type TabsTabState = { active: boolean; disabled: boolean; value: string };\n\ntype TabsTabNativeProps = Omit<\n  ComponentPropsWithoutRef<\"button\">,\n  | \"aria-controls\"\n  | \"aria-selected\"\n  | \"children\"\n  | \"className\"\n  | \"disabled\"\n  | \"id\"\n  | \"onClick\"\n  | \"onFocus\"\n  | \"ref\"\n  | \"role\"\n  | \"style\"\n  | \"tabIndex\"\n  | \"type\"\n  | \"value\"\n>;\n\nexport type TabsTabProps = HeidiIntrinsicHostProps<TabsTabState, \"button\"> &\n  TabsTabNativeProps & {\n    \"aria-controls\"?: string;\n    children?: ReactNode;\n    disabled?: boolean;\n    id?: string;\n    onClick?: ComponentPropsWithoutRef<\"button\">[\"onClick\"];\n    onFocus?: ComponentPropsWithoutRef<\"button\">[\"onFocus\"];\n    ref?: Ref<HTMLButtonElement>;\n    value: string;\n  };\n\nexport function TabsTab({\n  \"aria-controls\": ariaControls,\n  children,\n  className,\n  disabled = false,\n  id: idProp,\n  onClick,\n  onFocus,\n  ref,\n  render,\n  style,\n  value,\n  ...nativeProps\n}: TabsTabProps) {\n  const initialTabStopId = useContext(TabsListInitialTabStopContext);\n  const {\n    idPrefix,\n    panelIdForValue,\n    registerTab,\n    select,\n    selected,\n    setTabbableId,\n    tabbableId: registeredTabbableId,\n    unregisterTab\n  } = useTabsContext(\"Tab\");\n  const id = idProp ?? `${idPrefix}-tab-${encodeValueForId(value)}`;\n  const active = selected === value;\n  const tabbableId = registeredTabbableId ?? initialTabStopId ?? (active && !disabled ? id : null);\n\n  useEffect(() => {\n    registerTab({ disabled, id, value });\n    return () => unregisterTab(id);\n  }, [disabled, id, registerTab, unregisterTab, value]);\n\n  const handleClick = composeHeidiEventHandlers(onClick, (event: MouseEvent<HTMLButtonElement>) => {\n    if (disabled) {\n      event.preventDefault();\n      return;\n    }\n    select(value);\n  });\n  const handleFocus = composeHeidiEventHandlers(\n    onFocus,\n    (_event: FocusEvent<HTMLButtonElement>) => {\n      if (!disabled) {\n        setTabbableId(id);\n      }\n    }\n  );\n\n  return renderHeidiElement({\n    className: TABS_CLASSES.tab,\n    dataPart: \"tabs-tab\",\n    element: \"button\",\n    props: {\n      ...nativeProps,\n      \"aria-controls\": ariaControls ?? panelIdForValue(value),\n      \"aria-selected\": active,\n      children,\n      \"data-state\": active ? \"active\" : \"inactive\",\n      \"data-value\": value,\n      id,\n      onClick: handleClick,\n      onFocus: handleFocus,\n      ref,\n      role: \"tab\",\n      tabIndex: !disabled && tabbableId === id ? 0 : -1,\n      type: \"button\",\n      ...nativeDisabledAttrs(disabled)\n    },\n    renderProps: { className, render, style },\n    state: { active, disabled, value }\n  });\n}\n\n/** State handed to a `TabsPanel` className/style/render callback. */\nexport type TabsPanelState = { active: boolean; value: string };\n\ntype TabsPanelNativeProps = Omit<\n  ComponentPropsWithoutRef<\"div\">,\n  \"aria-labelledby\" | \"children\" | \"className\" | \"id\" | \"ref\" | \"role\" | \"style\" | \"tabIndex\"\n>;\n\nexport type TabsPanelProps = HeidiIntrinsicHostProps<TabsPanelState, \"div\"> &\n  TabsPanelNativeProps & {\n    \"aria-labelledby\"?: string;\n    children?: ReactNode;\n    id?: string;\n    ref?: Ref<HTMLDivElement>;\n    tabIndex?: number;\n    value: string;\n  };\n\nexport function TabsPanel({\n  \"aria-labelledby\": ariaLabelledBy,\n  children,\n  className,\n  id: idProp,\n  ref,\n  render,\n  style,\n  tabIndex = 0,\n  value,\n  ...nativeProps\n}: TabsPanelProps) {\n  const { idPrefix, registerPanel, selected, tabIdForValue, unregisterPanel } = useTabsContext(\"Panel\");\n  const id = idProp ?? `${idPrefix}-panel-${encodeValueForId(value)}`;\n  const active = selected === value;\n\n  useEffect(() => {\n    registerPanel({ id, value });\n    return () => unregisterPanel(id);\n  }, [id, registerPanel, unregisterPanel, value]);\n\n  return renderHeidiElement({\n    className: TABS_CLASSES.panel,\n    dataPart: \"tabs-panel\",\n    element: \"div\",\n    props: {\n      ...nativeProps,\n      \"aria-labelledby\": ariaLabelledBy ?? tabIdForValue(value),\n      children,\n      \"data-state\": active ? \"active\" : \"inactive\",\n      id,\n      ref,\n      role: \"tabpanel\",\n      tabIndex\n    },\n    renderProps: { className, render, style },\n    state: { active, value }\n  });\n}\n\n/**\n * Namespace sugar for client components (`<Tabs.Root>`). From a Server\n * Component use the named exports (TabsRoot, …) instead.\n */\nexport const Tabs = {\n  List: TabsList,\n  Panel: TabsPanel,\n  Root: TabsRoot,\n  Tab: TabsTab\n} as const;\n",
      "path": "packages/heidi-ui/src/tabs/tabs.tsx",
      "target": "components/ui/heidi/tabs/tabs.tsx",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui tabs — STRUCTURAL CSS only (platform behavior).\n * Layout + inactive panel hide + space reservation for the active underline.\n * The list-border overlap (negative margin) lives in the THEME, not here:\n * base draws no list border, so a base-side negative margin pulled headless\n * tabs 1px over the panel below them with nothing to overlap.\n */\n\n@layer heidi-ui-base {\n  .hui-tabs-root {\n    display: flex;\n    flex-direction: column;\n    min-inline-size: 0;\n  }\n\n  .hui-tabs-root[data-orientation=\"horizontal\"] {\n    flex-direction: column;\n  }\n\n  .hui-tabs-root[data-orientation=\"vertical\"] {\n    flex-direction: row;\n  }\n\n  .hui-tabs-list {\n    display: flex;\n    max-inline-size: 100%;\n    overflow-x: auto;\n    overscroll-behavior-x: contain;\n  }\n\n  .hui-tabs-list[data-orientation=\"horizontal\"] {\n    flex-direction: row;\n  }\n\n  .hui-tabs-list[data-orientation=\"vertical\"] {\n    flex-direction: column;\n    max-block-size: 100%;\n    overflow-x: visible;\n    overflow-y: auto;\n    overscroll-behavior-x: auto;\n    overscroll-behavior-y: contain;\n  }\n\n  .hui-tabs-tab {\n    flex: 0 0 auto;\n    border: none;\n    /* reserves underline space so activating a tab never shifts layout */\n    border-block-end: 2px solid transparent;\n  }\n\n  .hui-tabs-panel[data-state=\"inactive\"] {\n    display: none !important;\n  }\n}\n",
      "path": "packages/heidi-ui/src/tabs/tabs.base.css",
      "target": "components/ui/heidi/tabs/tabs.base.css",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui tabs — VISUAL theme (opt-in). Consumes --hui-* semantic theme vars (wired via adapters/heidi.css).\n */\n\n@layer heidi-ui {\n  .hui-tabs-list {\n    border-block-end: var(--hui-border-width) var(--hui-border-style)\n      var(--hui-color-border-default);\n    gap: var(--hui-space-1);\n    scrollbar-width: thin;\n  }\n\n  .hui-tabs-list[data-orientation=\"vertical\"] {\n    border-block-end: 0;\n    border-inline-end: var(--hui-border-width) var(--hui-border-style)\n      var(--hui-color-border-default);\n  }\n\n  .hui-tabs-tab {\n    background: transparent;\n    border-block-end: var(--hui-border-width-strong) solid transparent;\n    border-radius: 0;\n    color: var(--hui-color-fg-muted);\n    cursor: pointer;\n    font: inherit;\n    margin-block-end: calc(-1 * var(--hui-border-width));\n    padding: var(--hui-space-2) var(--hui-space-3);\n  }\n\n  .hui-tabs-tab:focus-visible {\n    outline: var(--hui-focus-ring-width) solid var(--hui-color-focus-ring);\n    outline-offset: calc(-1 * var(--hui-focus-ring-offset));\n  }\n\n  .hui-tabs-tab[data-state=\"active\"] {\n    border-block-end-color: var(--hui-color-brand-primary);\n    color: var(--hui-color-fg-strong);\n  }\n\n  .hui-tabs-list[data-orientation=\"vertical\"] .hui-tabs-tab {\n    border-block-end: 0;\n    border-inline-end: var(--hui-border-width-strong) solid transparent;\n    margin-block-end: 0;\n    margin-inline-end: calc(-1 * var(--hui-border-width));\n    text-align: start;\n  }\n\n  .hui-tabs-list[data-orientation=\"vertical\"]\n    .hui-tabs-tab[data-state=\"active\"] {\n    border-inline-end-color: var(--hui-color-brand-primary);\n  }\n\n  .hui-tabs-tab:disabled,\n  .hui-tabs-tab[data-disabled=\"true\"] {\n    cursor: not-allowed;\n    opacity: 0.5;\n  }\n\n  .hui-tabs-panel {\n    color: var(--hui-color-fg-default);\n    padding: var(--hui-space-3) 0;\n  }\n\n  .hui-tabs-panel:focus-visible {\n    outline: var(--hui-focus-ring-width) solid var(--hui-color-focus-ring);\n    outline-offset: var(--hui-focus-ring-offset);\n  }\n\n  @media (forced-colors: active) {\n    .hui-tabs-list {\n      border-color: CanvasText;\n    }\n\n    .hui-tabs-tab[data-state=\"active\"] {\n      border-block-end-color: Highlight;\n      color: CanvasText;\n    }\n\n    .hui-tabs-list[data-orientation=\"vertical\"]\n      .hui-tabs-tab[data-state=\"active\"] {\n      border-inline-end-color: Highlight;\n    }\n\n    .hui-tabs-tab:focus-visible,\n    .hui-tabs-panel:focus-visible {\n      outline-color: Highlight;\n    }\n\n    .hui-tabs-tab:disabled,\n    .hui-tabs-tab[data-disabled=\"true\"] {\n      color: GrayText;\n      opacity: 1;\n    }\n  }\n}\n",
      "path": "packages/heidi-ui/src/tabs/tabs.theme.css",
      "target": "components/ui/heidi/tabs/tabs.theme.css",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui tabs — aggregator (base + Heidi adapter + theme).\n * Headless: import tabs.base.css only.\n * Themed: import this file (or heidi-ui/styles.css).\n */\n\n@import \"./tabs.base.css\";\n@import \"../../../../styles/heidi-ui-adapter.css\";\n@import \"./tabs.theme.css\";\n",
      "path": "packages/heidi-ui/src/tabs/tabs.css",
      "target": "components/ui/heidi/tabs/tabs.css",
      "type": "registry:ui"
    },
    {
      "content": "/** GENERATED from packages/heidi-ui/src/tabs/tabs.base.css + packages/heidi-ui/src/tabs/tabs.theme.css — do not edit by hand.\n * Rebuild: `bun run hui:build`. Freshness: `bun run contracts:check`. */\nexport const TABS_CLASSES = {\n  list: \"hui-tabs-list\",\n  panel: \"hui-tabs-panel\",\n  root: \"hui-tabs-root\",\n  tab: \"hui-tabs-tab\",\n} as const;\n",
      "path": "packages/heidi-ui/src/tabs/tabs.classes.generated.ts",
      "target": "components/ui/heidi/tabs/tabs.classes.generated.ts",
      "type": "registry:ui"
    },
    {
      "content": "/** GENERATED from tabs.anatomy.json + tabs.base.css + tabs.theme.css — do not edit by hand.\n * Rebuild: `bun run hui:build`. Freshness: `bun run contracts:check`. */\n\nexport type TabsDisabled = \"true\";\nexport type TabsOrientation = \"horizontal\" | \"vertical\";\nexport type TabsState = \"active\" | \"inactive\";\n\nexport const TABS_ANATOMY = {\n  \"component\": \"tabs\",\n  \"description\": \"ARIA APG tabs with nullable controlled or uncontrolled selection, one roving tab stop, horizontal or vertical navigation, automatic or manual activation, disabled-tab skipping, explicit external-panel wiring, and safe generated ids. Composition via native host props and renderHeidiElement.\",\n  \"schemaVersion\": 2,\n  \"parts\": {\n    \"list\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-label\",\n          \"aria-orientation\"\n        ],\n        \"role\": \"tablist\"\n      },\n      \"class\": \"hui-tabs-list\",\n      \"css\": {\n        \"structural\": [\n          \"display\",\n          \"flex-direction\",\n          \"overflow-x\",\n          \"overflow-y\"\n        ]\n      },\n      \"dataPart\": \"tabs-list\",\n      \"description\": \"Named tablist; owns orientation-aware keyboard navigation and skips disabled tabs.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"label\",\n        \"onKeyDown\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-orientation\": [\n          \"horizontal\",\n          \"vertical\"\n        ]\n      }\n    },\n    \"panel\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-labelledby\"\n        ],\n        \"role\": \"tabpanel\"\n      },\n      \"class\": \"hui-tabs-panel\",\n      \"css\": {\n        \"structural\": [\n          \"display\"\n        ]\n      },\n      \"dataPart\": \"tabs-panel\",\n      \"description\": \"Tab panel with safe or explicit id/label wiring; inactive panels are hidden and tabIndex remains consumer-selectable.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"aria-labelledby\",\n        \"className\",\n        \"id\",\n        \"ref\",\n        \"render\",\n        \"style\",\n        \"tabIndex\",\n        \"value\"\n      ],\n      \"pseudoStates\": [\n        \":focus-visible\"\n      ],\n      \"states\": {\n        \"data-state\": [\n          \"active\",\n          \"inactive\"\n        ]\n      }\n    },\n    \"root\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-tabs-root\",\n      \"css\": {\n        \"structural\": [\n          \"display\",\n          \"flex-direction\"\n        ]\n      },\n      \"dataPart\": \"tabs-root\",\n      \"description\": \"Selection and registration owner; null selection keeps one enabled tab tabbable without selecting it.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"activationMode\",\n        \"className\",\n        \"defaultValue\",\n        \"onValueChange\",\n        \"orientation\",\n        \"ref\",\n        \"render\",\n        \"style\",\n        \"value\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-orientation\": [\n          \"horizontal\",\n          \"vertical\"\n        ]\n      }\n    },\n    \"tab\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-controls\",\n          \"aria-selected\"\n        ],\n        \"role\": \"tab\"\n      },\n      \"class\": \"hui-tabs-tab\",\n      \"css\": {\n        \"structural\": [\n          \"border\",\n          \"border-block-end\",\n          \"margin-block-end\"\n        ]\n      },\n      \"dataPart\": \"tabs-tab\",\n      \"description\": \"Roving native button with safe or explicit id/controls wiring; disabled tabs are registered and skipped.\",\n      \"element\": \"button\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"aria-controls\",\n        \"className\",\n        \"disabled\",\n        \"id\",\n        \"onClick\",\n        \"onFocus\",\n        \"ref\",\n        \"render\",\n        \"style\",\n        \"value\"\n      ],\n      \"pseudoStates\": [\n        \":disabled\",\n        \":focus-visible\"\n      ],\n      \"states\": {\n        \"data-disabled\": [\n          \"true\"\n        ],\n        \"data-state\": [\n          \"active\",\n          \"inactive\"\n        ]\n      }\n    }\n  },\n  \"rootProps\": [\n    \"activationMode\",\n    \"defaultValue\",\n    \"onValueChange\",\n    \"orientation\",\n    \"value\"\n  ],\n  \"tokens\": [\n    \"--hui-border-style\",\n    \"--hui-border-width\",\n    \"--hui-border-width-strong\",\n    \"--hui-color-border-default\",\n    \"--hui-color-brand-primary\",\n    \"--hui-color-fg-default\",\n    \"--hui-color-fg-muted\",\n    \"--hui-color-fg-strong\",\n    \"--hui-color-focus-ring\",\n    \"--hui-focus-ring-offset\",\n    \"--hui-focus-ring-width\",\n    \"--hui-space-1\",\n    \"--hui-space-2\",\n    \"--hui-space-3\"\n  ]\n} as const;\n",
      "path": "packages/heidi-ui/src/tabs/tabs.anatomy.generated.ts",
      "target": "components/ui/heidi/tabs/tabs.anatomy.generated.ts",
      "type": "registry:ui"
    },
    {
      "content": "{\n  \"component\": \"tabs\",\n  \"description\": \"ARIA APG tabs with nullable controlled or uncontrolled selection, one roving tab stop, horizontal or vertical navigation, automatic or manual activation, disabled-tab skipping, explicit external-panel wiring, and safe generated ids. Composition via native host props and renderHeidiElement.\",\n  \"schemaVersion\": 2,\n  \"parts\": {\n    \"list\": {\n      \"aria\": {\n        \"owns\": [\"aria-label\", \"aria-orientation\"],\n        \"role\": \"tablist\"\n      },\n      \"class\": \"hui-tabs-list\",\n      \"css\": {\n        \"structural\": [\"display\", \"flex-direction\", \"overflow-x\", \"overflow-y\"]\n      },\n      \"dataPart\": \"tabs-list\",\n      \"description\": \"Named tablist; owns orientation-aware keyboard navigation and skips disabled tabs.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\"className\", \"label\", \"onKeyDown\", \"ref\", \"render\", \"style\"],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-orientation\": [\"horizontal\", \"vertical\"]\n      }\n    },\n    \"panel\": {\n      \"aria\": {\n        \"owns\": [\"aria-labelledby\"],\n        \"role\": \"tabpanel\"\n      },\n      \"class\": \"hui-tabs-panel\",\n      \"css\": {\n        \"structural\": [\"display\"]\n      },\n      \"dataPart\": \"tabs-panel\",\n      \"description\": \"Tab panel with safe or explicit id/label wiring; inactive panels are hidden and tabIndex remains consumer-selectable.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"aria-labelledby\",\n        \"className\",\n        \"id\",\n        \"ref\",\n        \"render\",\n        \"style\",\n        \"tabIndex\",\n        \"value\"\n      ],\n      \"pseudoStates\": [\":focus-visible\"],\n      \"states\": {\n        \"data-state\": [\"active\", \"inactive\"]\n      }\n    },\n    \"root\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-tabs-root\",\n      \"css\": {\n        \"structural\": [\"display\", \"flex-direction\"]\n      },\n      \"dataPart\": \"tabs-root\",\n      \"description\": \"Selection and registration owner; null selection keeps one enabled tab tabbable without selecting it.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"activationMode\",\n        \"className\",\n        \"defaultValue\",\n        \"onValueChange\",\n        \"orientation\",\n        \"ref\",\n        \"render\",\n        \"style\",\n        \"value\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-orientation\": [\"horizontal\", \"vertical\"]\n      }\n    },\n    \"tab\": {\n      \"aria\": {\n        \"owns\": [\"aria-controls\", \"aria-selected\"],\n        \"role\": \"tab\"\n      },\n      \"class\": \"hui-tabs-tab\",\n      \"css\": {\n        \"structural\": [\"border\", \"border-block-end\", \"margin-block-end\"]\n      },\n      \"dataPart\": \"tabs-tab\",\n      \"description\": \"Roving native button with safe or explicit id/controls wiring; disabled tabs are registered and skipped.\",\n      \"element\": \"button\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"aria-controls\",\n        \"className\",\n        \"disabled\",\n        \"id\",\n        \"onClick\",\n        \"onFocus\",\n        \"ref\",\n        \"render\",\n        \"style\",\n        \"value\"\n      ],\n      \"pseudoStates\": [\":disabled\", \":focus-visible\"],\n      \"states\": {\n        \"data-disabled\": [\"true\"],\n        \"data-state\": [\"active\", \"inactive\"]\n      }\n    }\n  },\n  \"rootProps\": [\n    \"activationMode\",\n    \"defaultValue\",\n    \"onValueChange\",\n    \"orientation\",\n    \"value\"\n  ]\n}\n",
      "path": "packages/heidi-ui/src/tabs/tabs.anatomy.json",
      "target": "components/ui/heidi/tabs/tabs.anatomy.json",
      "type": "registry:ui"
    },
    {
      "content": "/**\n * Cross-cutting disabled attrs for heidi-ui interactive hosts.\n *\n * House convention (Phase 23):\n * - Button widgets → native `disabled` + `data-disabled=\"true\"` (CSS + a11y).\n * - Non-button options (Select.Item) → `aria-disabled` + `data-disabled=\"true\"`\n *   (native disabled is invalid on role=option divs).\n * - Group roots may cascade `disabled` to children and mirror `data-disabled`.\n * - Keyboard nav / activation always skip disabled hosts.\n */\n\nexport type NativeDisabledAttrs = {\n  \"data-disabled\"?: true;\n  disabled?: true;\n};\n\nexport type AriaDisabledAttrs = {\n  \"aria-disabled\"?: true;\n  \"data-disabled\"?: true;\n};\n\nexport type DataDisabledAttrs = {\n  \"data-disabled\"?: true;\n};\n\n/** Native button/input disabled + styling hook. */\nexport function nativeDisabledAttrs(disabled: boolean | undefined): NativeDisabledAttrs {\n  if (!disabled) {\n    return {};\n  }\n  return {\n    \"data-disabled\": true,\n    disabled: true\n  };\n}\n\n/** ARIA-disabled for non-native hosts (e.g. role=option). */\nexport function ariaDisabledAttrs(disabled: boolean | undefined): AriaDisabledAttrs {\n  if (!disabled) {\n    return {};\n  }\n  return {\n    \"aria-disabled\": true,\n    \"data-disabled\": true\n  };\n}\n\n/** Styling hook only (group roots that cascade disabled). */\nexport function dataDisabledAttrs(disabled: boolean | undefined): DataDisabledAttrs {\n  if (!disabled) {\n    return {};\n  }\n  return {\n    \"data-disabled\": true\n  };\n}\n\nexport function isDisabledElement(el: Element | null | undefined): boolean {\n  if (!el || !(el instanceof HTMLElement)) {\n    return false;\n  }\n  if (el.matches(\":disabled\")) {\n    return true;\n  }\n  return el.getAttribute(\"aria-disabled\") === \"true\" || el.getAttribute(\"data-disabled\") === \"true\";\n}\n",
      "path": "packages/heidi-ui/src/_internal/disabled.ts",
      "target": "components/ui/heidi/_internal/disabled.ts",
      "type": "registry:ui"
    },
    {
      "content": "/**\n * Shared host rendering for heidi-ui parts — Base-UI-shaped composition\n * (docs/HEIDI-UI-HEADLESS.md § 4 / HEIDI-UI.md § 4.5):\n *   - className / style as value or (state) => value\n *   - render as ReactElement or (props, state) => ReactElement\n *   - ref forwarding\n *   - data-hui-part always set (stable unstyled hook)\n *\n * Structural inline styles (e.g. anchorName) are merged last so a consumer\n * style override cannot drop platform wiring.\n */\n\nimport {\n  cloneElement,\n  createElement,\n  isValidElement,\n  type CSSProperties,\n  type ComponentPropsWithRef,\n  type HTMLAttributes,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  type RefCallback\n} from \"react\";\n\nexport type HeidiClassName<State> = string | ((state: State) => string | undefined) | undefined;\n\nexport type HeidiStyle<State> =\n  | CSSProperties\n  | ((state: State) => CSSProperties | undefined)\n  | undefined;\n\nexport type HeidiRenderFn<State, Props> = (\n  props: Props,\n  state: State\n) => ReactElement;\n\nexport type HeidiRender<State, Props> =\n  | ReactElement\n  | HeidiRenderFn<State, Props>\n  | undefined;\n\ntype DefaultRenderProps = HTMLAttributes<HTMLElement> & { ref?: Ref<HTMLElement> };\n\nexport type HeidiHostProps<\n  State,\n  RenderProps extends object = DefaultRenderProps\n> = {\n  className?: HeidiClassName<State>;\n  render?: HeidiRender<State, RenderProps>;\n  style?: HeidiStyle<State>;\n};\n\nexport type HeidiIntrinsicHostProps<\n  State,\n  Tag extends keyof HTMLElementTagNameMap\n> = HeidiHostProps<State, ComponentPropsWithRef<Tag>>;\n\nfunction resolveClassName<State>(\n  className: HeidiClassName<State>,\n  state: State\n): string | undefined {\n  return typeof className === \"function\" ? className(state) : className;\n}\n\nfunction resolveStyle<State>(style: HeidiStyle<State>, state: State): CSSProperties | undefined {\n  return typeof style === \"function\" ? style(state) : style;\n}\n\nfunction mergeClassNames(...parts: Array<string | undefined>): string | undefined {\n  const merged = parts.filter(Boolean).join(\" \");\n  return merged.length > 0 ? merged : undefined;\n}\n\n/**\n * Compose a consumer event with library behavior. Consumers run first and may\n * cancel the component behavior with `event.preventDefault()`. Heidi keeps\n * this native-event convention intentionally: it avoids a second branded event\n * API while still making async/controlled actions vetoable.\n */\nexport function composeHeidiEventHandlers<Event extends { defaultPrevented: boolean }>(\n  consumer: ((event: Event) => void) | undefined,\n  library: (event: Event) => void\n): (event: Event) => void {\n  return (event) => {\n    consumer?.(event);\n    if (!event.defaultPrevented) {\n      library(event);\n    }\n  };\n}\n\n/** React 19-safe ref fan-out, including callback-ref cleanup functions. */\nexport function mergeHeidiRefs<Element>(\n  ...inputRefs: Array<Ref<Element> | undefined>\n): Ref<Element> | undefined {\n  const refs = Array.from(\n    new Set(\n      inputRefs.filter(\n        (ref): ref is Exclude<Ref<Element>, null> => ref != null\n      )\n    )\n  );\n  if (refs.length === 0) {\n    return undefined;\n  }\n  if (refs.length === 1) {\n    return refs[0];\n  }\n\n  let cache = mergedRefCache;\n  for (const ref of refs) {\n    const key = ref as object;\n    let child = cache.children.get(key);\n    if (!child) {\n      child = { children: new WeakMap() };\n      cache.children.set(key, child);\n    }\n    cache = child;\n  }\n  if (cache.callback) {\n    return cache.callback as RefCallback<Element>;\n  }\n\n  const callback: RefCallback<Element> = (node) => {\n    const cleanups: Array<() => void> = [];\n    for (const ref of refs) {\n      if (typeof ref === \"function\") {\n        const cleanup = ref(node);\n        if (node !== null) {\n          cleanups.push(typeof cleanup === \"function\" ? cleanup : () => ref(null));\n        }\n      } else if (ref) {\n        ref.current = node;\n        if (node !== null) {\n          cleanups.push(() => {\n            ref.current = null;\n          });\n        }\n      }\n    }\n    return cleanups.length > 0\n      ? () => {\n          for (const cleanup of cleanups) {\n            cleanup();\n          }\n        }\n      : undefined;\n  };\n  cache.callback = callback as RefCallback<unknown>;\n  return callback;\n}\n\ntype MergedRefCache = {\n  callback?: RefCallback<unknown>;\n  children: WeakMap<object, MergedRefCache>;\n};\n\nconst mergedRefCache: MergedRefCache = { children: new WeakMap() };\n\ntype UnknownHandler = (...args: never[]) => unknown;\n\nfunction isEventHandler(key: string, value: unknown): value is UnknownHandler {\n  return /^on[A-Z]/.test(key) && typeof value === \"function\";\n}\n\nfunction defaultPrevented(args: unknown[]): boolean {\n  const event = args[0];\n  return (\n    typeof event === \"object\" &&\n    event !== null &&\n    \"defaultPrevented\" in event &&\n    event.defaultPrevented === true\n  );\n}\n\nfunction composeUnknownHandlers(\n  consumer: UnknownHandler,\n  library: UnknownHandler\n): UnknownHandler {\n  if (consumer === library) {\n    return library;\n  }\n  return ((...args: unknown[]) => {\n    (consumer as (...handlerArgs: unknown[]) => unknown)(...args);\n    if (!defaultPrevented(args)) {\n      (library as (...handlerArgs: unknown[]) => unknown)(...args);\n    }\n  }) as UnknownHandler;\n}\n\n/** Library-owned host props — permissive so button `type`, `data-*`, `popover`, etc. type-check. */\nexport type HeidiElementProps<Tag extends keyof HTMLElementTagNameMap> = Record<\n  string,\n  unknown\n> & {\n  children?: ReactNode;\n  className?: string;\n  ref?: Ref<HTMLElementTagNameMap[Tag]>;\n  style?: CSSProperties;\n};\n\ntype RenderElementParams<\n  State,\n  Tag extends keyof HTMLElementTagNameMap,\n  RenderProps extends object = DefaultRenderProps\n> = {\n  /** Default class from the generated hui-* map (theme/base target). */\n  className?: string;\n  /** Stable machine hook — always emitted. */\n  dataPart: string;\n  /** Intrinsic tag when `render` is omitted. */\n  element: Tag;\n  /** Props the library owns (aria, ids, handlers, popover, …). */\n  props: HeidiElementProps<Tag>;\n  /** Optional consumer composition props. */\n  renderProps?: HeidiHostProps<State, RenderProps>;\n  /** Typed state passed to functional className/style/render. */\n  state: State;\n  /** Structural inline styles that must survive consumer style merges. */\n  structuralStyle?: CSSProperties;\n  /**\n   * Render-element handlers to remove instead of composing. This is reserved\n   * for states such as a focusable disabled composite item where the public\n   * contract requires press handlers to be completely inert.\n   */\n  suppressRenderedHandlers?: readonly string[];\n};\n\n/**\n * Render a heidi-ui host element with optional Base-shaped composition.\n */\nexport function renderHeidiElement<\n  State,\n  Tag extends keyof HTMLElementTagNameMap,\n  RenderProps extends object = DefaultRenderProps\n>(\n  params: RenderElementParams<State, Tag, RenderProps>\n): ReactElement {\n  const {\n    className,\n    dataPart,\n    element,\n    props,\n    renderProps,\n    state,\n    structuralStyle,\n    suppressRenderedHandlers\n  } = params;\n  const consumerClass = resolveClassName(renderProps?.className, state);\n  const consumerStyle = resolveStyle(renderProps?.style, state);\n  const mergedStyle: CSSProperties | undefined =\n    props.style || consumerStyle || structuralStyle\n      ? { ...props.style, ...consumerStyle, ...structuralStyle }\n      : undefined;\n\n  const outProps: HTMLAttributes<HTMLElement> & {\n    \"data-hui-part\": string;\n    ref?: Ref<HTMLElement>;\n  } = {\n    ...(props as HTMLAttributes<HTMLElement> & { ref?: Ref<HTMLElement> }),\n    className: mergeClassNames(consumerClass, className),\n    \"data-hui-part\": dataPart,\n    style: mergedStyle\n  };\n\n  const render = renderProps?.render;\n  if (typeof render === \"function\") {\n    // Post-merge the returned element as well. This deliberately makes the\n    // stable data hook, owned semantics, refs, and internal handlers survive\n    // even when a render function forgets to spread one of the supplied props.\n    return mergeRenderedElement(\n      render(outProps as unknown as RenderProps, state),\n      outProps,\n      dataPart,\n      structuralStyle,\n      suppressRenderedHandlers\n    );\n  }\n  if (isValidElement(render)) {\n    return mergeRenderedElement(\n      render,\n      outProps,\n      dataPart,\n      structuralStyle,\n      suppressRenderedHandlers\n    );\n  }\n\n  return createElement(element, outProps as never, props.children);\n}\n\ntype RenderedProps = Record<string, unknown> & {\n  children?: ReactNode;\n  className?: string;\n  ref?: Ref<HTMLElement>;\n  style?: CSSProperties;\n};\n\nfunction mergeRenderedElement(\n  element: ReactElement,\n  libraryProps: HTMLAttributes<HTMLElement> & {\n    \"data-hui-part\": string;\n    ref?: Ref<HTMLElement>;\n  },\n  dataPart: string,\n  structuralStyle: CSSProperties | undefined,\n  suppressRenderedHandlers: readonly string[] | undefined\n): ReactElement {\n  const rendered = element as ReactElement<RenderedProps>;\n  const renderedProps = rendered.props;\n  const renderedClass =\n    renderedProps.className === libraryProps.className\n      ? undefined\n      : renderedProps.className;\n  const merged: RenderedProps = {\n    ...renderedProps,\n    ...libraryProps,\n    className: mergeClassNames(renderedClass, libraryProps.className),\n    \"data-hui-part\": dataPart,\n    ref: mergeHeidiRefs(libraryProps.ref, renderedProps.ref),\n    style: {\n      ...libraryProps.style,\n      ...renderedProps.style,\n      ...structuralStyle\n    }\n  };\n\n  if (Object.prototype.hasOwnProperty.call(renderedProps, \"children\")) {\n    merged.children = renderedProps.children;\n  }\n\n  const suppressedHandlers = suppressRenderedHandlers\n    ? new Set(suppressRenderedHandlers)\n    : null;\n  for (const key of new Set([...Object.keys(renderedProps), ...Object.keys(libraryProps)])) {\n    const consumer = renderedProps[key];\n    const library = (libraryProps as unknown as Record<string, unknown>)[key];\n    if (suppressedHandlers?.has(key)) {\n      merged[key] = library;\n      continue;\n    }\n    if (isEventHandler(key, consumer) && isEventHandler(key, library)) {\n      merged[key] = composeUnknownHandlers(consumer, library);\n    }\n  }\n\n  return cloneElement(rendered, merged as never);\n}\n",
      "path": "packages/heidi-ui/src/_internal/render-element.ts",
      "target": "components/ui/heidi/_internal/render-element.ts",
      "type": "registry:ui"
    },
    {
      "content": "/**\n * Shared roving-tabindex arrow navigation (P7).\n *\n * Radio, Tabs, Toolbar and ToggleGroup each declared `NAVIGATION_KEYS`\n * byte-identically, each wrote the same enabled-descendants query with two\n * different selector strings, and each resolved arrow keys to a target index.\n *\n * ponytail: the audit listed the resolvers as four divergent implementations —\n * \"radio treats ArrowDown as forward regardless of orientation; toggle-group\n * accepts both axes when horizontal; tabs restricts to its axis\". Normalising\n * the spellings shows THREE of the four agree exactly. Radio's\n * `ArrowDown || (rtl ? ArrowLeft : ArrowRight)` and Toolbar's\n * `(horizontal ? horizontalForward : \"ArrowDown\") || (horizontal && \"ArrowDown\")\n * || (!horizontal && \"ArrowRight\")` accept the identical key set in both\n * orientations and both directions; they only look different. The single real\n * divergence is Tabs, which returns early on a cross-axis key. So the option\n * that matters is `crossAxis`, and it is false in exactly one place.\n *\n * That distinction is deliberate and worth keeping: a Toolbar is a\n * two-dimensional cluster where reaching for either axis is reasonable, while\n * a tablist has one axis and a cross-axis arrow should stay available to the\n * page (scrolling, or an outer roving container).\n *\n * `rtl` is read per event rather than per render because `direction` can change\n * without a React update, and it is gated on horizontal orientation: on a\n * vertical list the inline direction says nothing about which way is \"next\".\n */\n\nexport const NAVIGATION_KEYS = [\n  \"ArrowDown\",\n  \"ArrowLeft\",\n  \"ArrowRight\",\n  \"ArrowUp\",\n  \"End\",\n  \"Home\"\n];\n\n/**\n * Enabled items owned by THIS container — `closest()` re-anchors the match so\n * a nested roving container's items never join its ancestor's ring.\n */\nexport function collectRovingItems<TItem extends HTMLElement>(\n  container: HTMLElement,\n  itemSelector: string,\n  rootSelector: string,\n  isDisabled: (element: HTMLElement) => boolean\n): TItem[] {\n  return [...container.querySelectorAll<TItem>(itemSelector)].filter(\n    (element) =>\n      !isDisabled(element) && element.closest(rootSelector) === container\n  );\n}\n\nexport type RovingResolveOptions = {\n  /** Accept the perpendicular axis too. False only for Tabs. */\n  crossAxis: boolean;\n  orientation: \"horizontal\" | \"vertical\";\n};\n\n/**\n * The index this key should move focus to, or `null` when the key does not\n * apply — the caller must then return WITHOUT calling `preventDefault`, so an\n * unhandled arrow stays available to the page.\n */\nexport function resolveRovingIndex(\n  items: readonly HTMLElement[],\n  activeElement: Element | null,\n  key: string,\n  container: HTMLElement,\n  { crossAxis, orientation }: RovingResolveOptions\n): number | null {\n  if (items.length === 0) {\n    return null;\n  }\n  if (key === \"Home\") {\n    return 0;\n  }\n  if (key === \"End\") {\n    return items.length - 1;\n  }\n  const horizontal = orientation === \"horizontal\";\n  const rtl =\n    horizontal && getComputedStyle(container).direction === \"rtl\";\n  const inlineForward = rtl ? \"ArrowLeft\" : \"ArrowRight\";\n  const inlineBackward = rtl ? \"ArrowRight\" : \"ArrowLeft\";\n  const forwardKey = horizontal ? inlineForward : \"ArrowDown\";\n  const backwardKey = horizontal ? inlineBackward : \"ArrowUp\";\n  const crossForward = horizontal ? \"ArrowDown\" : inlineForward;\n  const crossBackward = horizontal ? \"ArrowUp\" : inlineBackward;\n\n  const current = items.indexOf(activeElement as HTMLElement);\n  if (key === forwardKey || (crossAxis && key === crossForward)) {\n    return current === -1 ? 0 : (current + 1) % items.length;\n  }\n  if (key === backwardKey || (crossAxis && key === crossBackward)) {\n    return (current <= 0 ? items.length : current) - 1;\n  }\n  return null;\n}\n",
      "path": "packages/heidi-ui/src/_internal/roving-focus.ts",
      "target": "components/ui/heidi/_internal/roving-focus.ts",
      "type": "registry:ui"
    },
    {
      "content": "/**\n * Shared id sanitizer for heidi-ui hosts.\n *\n * React's useId() emits characters that are invalid in HTML id / attribute\n * selectors and in CSS identifiers (`«r0»` in React 19, `:r0:` earlier).\n * heidi-ui interpolates these ids into element ids, aria relationships, and\n * dashed-ident anchor names (`--hui-*-anchor-…`), so everything outside\n * [a-zA-Z0-9_-] is stripped.\n */\n\nexport function safeId(id: string): string {\n  return id.replace(/[^a-zA-Z0-9_-]/g, \"\");\n}\n",
      "path": "packages/heidi-ui/src/_internal/safe-id.ts",
      "target": "components/ui/heidi/_internal/safe-id.ts",
      "type": "registry:ui"
    },
    {
      "content": "import { useEffect, useLayoutEffect } from \"react\";\n\n/**\n * `useLayoutEffect` that does not warn on the server.\n *\n * ponytail: fifteen primitives imported `useLayoutEffect` straight from React.\n * Every one of them carries `\"use client\"`, which is easy to misread as\n * \"client-only\" — in the App Router it means \"hydrated on the client\", and the\n * component is still rendered to HTML on the server first. React logs\n * \"useLayoutEffect does nothing on the server\" for each one, so a consumer\n * doing SSR saw a wall of warnings the app that ships this library never saw,\n * because it renders these routes on the client path.\n *\n * The branch is evaluated ONCE at module scope, not per render, and it is\n * therefore stable across a component's lifetime — swapping which hook is\n * called between renders would violate the rules of hooks. `typeof document`\n * rather than `typeof window`: both work, but `document` is the thing the\n * effect actually needs, and it keeps the check honest in exotic runtimes that\n * define a partial `window`.\n *\n * Rejected: `useInsertionEffect`, which runs earlier but is specified for\n * style injection and is not a general layout hook; and per-file guards, which\n * is what the 15 copies would have become.\n */\nexport const useHeidiLayoutEffect =\n  typeof document === \"undefined\" ? useEffect : useLayoutEffect;\n",
      "path": "packages/heidi-ui/src/_internal/use-layout-effect.ts",
      "target": "components/ui/heidi/_internal/use-layout-effect.ts",
      "type": "registry:ui"
    }
  ],
  "name": "tabs",
  "registryDependencies": [
    "@heidi-ui/heidi-tokens"
  ],
  "title": "Tabs",
  "type": "registry:ui"
}
