{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "dependencies": [],
  "description": "Base-UI-shaped single disclosure: Root groups a named Trigger and a lazily mounted Panel. Supports authoritative controlled state, cancellable change details, focusable disabled triggers, non-button composition, keepMounted, hidden-until-found reveal, measured transition hooks, and full native host props. Use Accordion for coordinated multi-item disclosure.",
  "files": [
    {
      "content": "\"use client\";\n\n/**\n * heidi-ui Collapsible — a single disclosure panel controlled by a button.\n *\n * The default closed panel is not rendered. Panel retention, find-in-page\n * reveal, measured CSS motion, controlled state, and non-button composition\n * are explicit opt-ins that follow the Base UI 1.6 disclosure contract.\n */\n\nimport {\n  type ComponentPropsWithoutRef,\n  createContext,\n  type KeyboardEvent,\n  type MouseEvent,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState\n} from \"react\";\nimport {\n  composeHeidiEventHandlers,\n  mergeHeidiRefs,\n  renderHeidiElement,\n  type HeidiIntrinsicHostProps\n} from \"../_internal/render-element\";\nimport { waitForElementAnimations } from \"../_internal/animation-wait\";\nimport { safeId } from \"../_internal/safe-id\";\nimport { useHeidiLayoutEffect } from \"../_internal/use-layout-effect\";\nimport { COLLAPSIBLE_CLASSES } from \"./collapsible.classes.generated\";\n\nexport type CollapsibleTransitionStatus = \"ending\" | \"idle\" | \"starting\" | undefined;\nexport type CollapsibleRootChangeEventReason = \"none\" | \"trigger-press\";\n\nexport type CollapsibleRootChangeEventDetails = {\n  /** Allows propagation in integrations that otherwise stop the source event. */\n  allowPropagation: () => void;\n  /** Cancels Heidi's uncontrolled state update. */\n  cancel: () => void;\n  /** Native event that requested the state change. */\n  event: Event;\n  /** Whether `cancel()` has been called. */\n  readonly isCanceled: boolean;\n  /** Whether `allowPropagation()` has been called. */\n  readonly isPropagationAllowed: boolean;\n  /** Why the open state was requested. */\n  reason: CollapsibleRootChangeEventReason;\n  /** Element that initiated the request, when one exists. */\n  trigger: Element | undefined;\n};\n\nfunction createChangeEventDetails(\n  reason: CollapsibleRootChangeEventReason,\n  event: Event,\n  trigger?: Element\n): CollapsibleRootChangeEventDetails {\n  let canceled = false;\n  let propagationAllowed = false;\n  return {\n    allowPropagation: () => {\n      propagationAllowed = true;\n    },\n    cancel: () => {\n      canceled = true;\n    },\n    event,\n    get isCanceled() {\n      return canceled;\n    },\n    get isPropagationAllowed() {\n      return propagationAllowed;\n    },\n    reason,\n    trigger\n  };\n}\n\nfunction dataState(open: boolean): \"closed\" | \"open\" {\n  return open ? \"open\" : \"closed\";\n}\n\nfunction openStateAttributes(open: boolean): Record<string, true | undefined> {\n  return open\n    ? { \"data-closed\": undefined, \"data-open\": true }\n    : { \"data-closed\": true, \"data-open\": undefined };\n}\n\nfunction transitionAttributes(\n  status: CollapsibleTransitionStatus\n): Record<string, true | undefined> {\n  return {\n    \"data-ending-style\": status === \"ending\" ? true : undefined,\n    \"data-starting-style\": status === \"starting\" ? true : undefined\n  };\n}\n\ntype CollapsibleContextValue = {\n  defaultPanelId: string;\n  disabled: boolean;\n  open: boolean;\n  panelId: string;\n  requestOpen: (\n    next: boolean,\n    reason: CollapsibleRootChangeEventReason,\n    event: Event,\n    trigger?: Element\n  ) => boolean;\n  setPanelIdOverride: (id: string | undefined) => void;\n  setTransitionStatus: (status: CollapsibleTransitionStatus) => void;\n  transitionStatus: CollapsibleTransitionStatus;\n};\n\nconst CollapsibleContext = createContext<CollapsibleContextValue | null>(null);\n\nfunction useCollapsibleContext(part: string): CollapsibleContextValue {\n  const context = useContext(CollapsibleContext);\n  if (!context) {\n    throw new Error(`Collapsible.${part} must be rendered inside Collapsible.Root.`);\n  }\n  return context;\n}\n\ntype NativeHostProps<Tag extends keyof HTMLElementTagNameMap> = Omit<\n  ComponentPropsWithoutRef<Tag>,\n  \"children\" | \"className\" | \"ref\" | \"style\"\n>;\n\nexport type CollapsibleRootState = {\n  disabled: boolean;\n  open: boolean;\n  transitionStatus: CollapsibleTransitionStatus;\n};\n\nexport type CollapsibleRootProps = HeidiIntrinsicHostProps<\n  CollapsibleRootState,\n  \"div\"\n> &\n  NativeHostProps<\"div\"> & {\n    children?: ReactNode;\n    defaultOpen?: boolean;\n    disabled?: boolean;\n    onOpenChange?: (\n      open: boolean,\n      details: CollapsibleRootChangeEventDetails\n    ) => void;\n    open?: boolean;\n    ref?: Ref<HTMLDivElement>;\n  };\n\nexport function CollapsibleRoot({\n  children,\n  className,\n  defaultOpen = false,\n  disabled = false,\n  onOpenChange,\n  open: openProp,\n  ref,\n  render,\n  style,\n  ...nativeProps\n}: CollapsibleRootProps) {\n  const generatedId = safeId(useId());\n  const defaultPanelId = `hui-collapsible-${generatedId}`;\n  const [panelIdOverride, setPanelIdOverride] = useState<string>();\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [transitionStatus, setTransitionStatusState] =\n    useState<CollapsibleTransitionStatus>(defaultOpen || openProp ? \"idle\" : undefined);\n  const controlled = openProp !== undefined;\n  const open = openProp ?? internalOpen;\n  const openRef = useRef(open);\n  openRef.current = open;\n\n  const setTransitionStatus = useCallback((next: CollapsibleTransitionStatus) => {\n    setTransitionStatusState((current) => (current === next ? current : next));\n  }, []);\n\n  const requestOpen = useCallback(\n    (\n      next: boolean,\n      reason: CollapsibleRootChangeEventReason,\n      event: Event,\n      trigger?: Element\n    ) => {\n      if (next === openRef.current) {\n        return false;\n      }\n      const details = createChangeEventDetails(reason, event, trigger);\n      onOpenChange?.(next, details);\n      if (details.isCanceled) {\n        return false;\n      }\n      if (!controlled) {\n        openRef.current = next;\n        setInternalOpen(next);\n      }\n      return true;\n    },\n    [controlled, onOpenChange]\n  );\n\n  const state = useMemo<CollapsibleRootState>(\n    () => ({ disabled, open, transitionStatus }),\n    [disabled, open, transitionStatus]\n  );\n  const value = useMemo<CollapsibleContextValue>(\n    () => ({\n      defaultPanelId,\n      disabled,\n      open,\n      panelId: panelIdOverride ?? defaultPanelId,\n      requestOpen,\n      setPanelIdOverride,\n      setTransitionStatus,\n      transitionStatus\n    }),\n    [\n      defaultPanelId,\n      disabled,\n      open,\n      panelIdOverride,\n      requestOpen,\n      setTransitionStatus,\n      transitionStatus\n    ]\n  );\n\n  return (\n    <CollapsibleContext value={value}>\n      {renderHeidiElement({\n        className: COLLAPSIBLE_CLASSES.root,\n        dataPart: \"collapsible-root\",\n        element: \"div\",\n        props: {\n          ...nativeProps,\n          ...openStateAttributes(open),\n          ...transitionAttributes(transitionStatus),\n          children,\n          \"data-disabled\": disabled ? true : undefined,\n          \"data-state\": dataState(open),\n          ref\n        },\n        renderProps: { className, render, style },\n        state\n      })}\n    </CollapsibleContext>\n  );\n}\n\nexport type CollapsibleTriggerState = CollapsibleRootState;\n\ntype CollapsibleTriggerNativeProps = Omit<\n  NativeHostProps<\"button\">,\n  \"aria-controls\" | \"aria-expanded\" | \"disabled\" | \"type\"\n>;\n\nexport type CollapsibleTriggerProps = HeidiIntrinsicHostProps<\n  CollapsibleTriggerState,\n  \"button\"\n> &\n  CollapsibleTriggerNativeProps & {\n    children?: ReactNode;\n    /** Override Root disabled state for this Trigger. */\n    disabled?: boolean;\n    /** Set false only when `render` returns a non-button host. */\n    nativeButton?: boolean;\n    ref?: Ref<HTMLButtonElement>;\n  };\n\nexport function CollapsibleTrigger({\n  children,\n  className,\n  disabled: disabledProp,\n  nativeButton = true,\n  onClick,\n  onKeyDown,\n  onKeyUp,\n  ref,\n  render,\n  style,\n  tabIndex,\n  ...nativeProps\n}: CollapsibleTriggerProps) {\n  const context = useCollapsibleContext(\"Trigger\");\n  const disabled = disabledProp ?? context.disabled;\n  const state = useMemo<CollapsibleTriggerState>(\n    () => ({\n      disabled,\n      open: context.open,\n      transitionStatus: context.transitionStatus\n    }),\n    [context.open, context.transitionStatus, disabled]\n  );\n\n  const handleClick = composeHeidiEventHandlers(onClick, (event: MouseEvent<HTMLButtonElement>) => {\n    if (disabled) {\n      event.preventDefault();\n      return;\n    }\n    context.requestOpen(\n      !context.open,\n      \"trigger-press\",\n      event.nativeEvent,\n      event.currentTarget\n    );\n  });\n\n  const handleKeyDown = composeHeidiEventHandlers(\n    onKeyDown,\n    (event: KeyboardEvent<HTMLButtonElement>) => {\n      if (nativeButton || event.target !== event.currentTarget) {\n        return;\n      }\n      if (event.key === \" \") {\n        event.preventDefault();\n        return;\n      }\n      if (event.key === \"Enter\") {\n        event.preventDefault();\n        if (!disabled && !event.repeat) {\n          event.currentTarget.click();\n        }\n      }\n    }\n  );\n\n  const handleKeyUp = composeHeidiEventHandlers(\n    onKeyUp,\n    (event: KeyboardEvent<HTMLButtonElement>) => {\n      if (\n        nativeButton ||\n        event.target !== event.currentTarget ||\n        event.key !== \" \"\n      ) {\n        return;\n      }\n      event.preventDefault();\n      if (!disabled) {\n        event.currentTarget.click();\n      }\n    }\n  );\n\n  return renderHeidiElement({\n    className: COLLAPSIBLE_CLASSES.trigger,\n    dataPart: \"collapsible-trigger\",\n    element: \"button\",\n    props: {\n      ...nativeProps,\n      \"aria-controls\": context.open ? context.panelId : undefined,\n      \"aria-disabled\": disabled ? true : undefined,\n      \"aria-expanded\": context.open,\n      children,\n      \"data-disabled\": disabled ? true : undefined,\n      \"data-panel-open\": context.open ? true : undefined,\n      \"data-state\": dataState(context.open),\n      ...transitionAttributes(context.transitionStatus),\n      onClick: handleClick,\n      onKeyDown: handleKeyDown,\n      onKeyUp: handleKeyUp,\n      ref,\n      role: nativeButton ? undefined : \"button\",\n      tabIndex: nativeButton ? tabIndex : (tabIndex ?? 0),\n      type: nativeButton ? \"button\" : undefined\n    },\n    renderProps: { className, render, style },\n    state\n  });\n}\n\nconst PANEL_HEIGHT = \"--collapsible-panel-height\";\nconst PANEL_WIDTH = \"--collapsible-panel-width\";\n\nfunction setPanelDimensions(panel: HTMLElement, value: \"auto\" | \"measured\") {\n  if (value === \"auto\") {\n    panel.style.setProperty(PANEL_HEIGHT, \"auto\");\n    panel.style.setProperty(PANEL_WIDTH, \"auto\");\n    return;\n  }\n  panel.style.setProperty(PANEL_HEIGHT, `${panel.scrollHeight}px`);\n  panel.style.setProperty(PANEL_WIDTH, `${panel.scrollWidth}px`);\n}\n\n\n\n\n\nexport type CollapsiblePanelState = CollapsibleRootState;\n\ntype CollapsiblePanelNativeProps = Omit<NativeHostProps<\"div\">, \"hidden\" | \"id\">;\n\nexport type CollapsiblePanelProps = HeidiIntrinsicHostProps<\n  CollapsiblePanelState,\n  \"div\"\n> &\n  CollapsiblePanelNativeProps & {\n    children?: ReactNode;\n    /** Retain the closed panel with the HTML hidden attribute. */\n    keepMounted?: boolean;\n    /** Retain the closed panel as `hidden=\"until-found\"` for browser find. */\n    hiddenUntilFound?: boolean;\n    id?: string;\n    ref?: Ref<HTMLDivElement>;\n  };\n\nexport function CollapsiblePanel({\n  children,\n  className,\n  hiddenUntilFound = false,\n  id,\n  keepMounted = false,\n  ref,\n  render,\n  style,\n  ...nativeProps\n}: CollapsiblePanelProps) {\n  const context = useCollapsibleContext(\"Panel\");\n  const requestOpen = context.requestOpen;\n  const setPanelIdOverride = context.setPanelIdOverride;\n  const setRootTransitionStatus = context.setTransitionStatus;\n  const panelRef = useRef<HTMLDivElement | null>(null);\n  const mergedRef = mergeHeidiRefs(panelRef, ref);\n  const previousOpenRef = useRef(context.open);\n  const openRef = useRef(context.open);\n  openRef.current = context.open;\n  const cycleRef = useRef(0);\n  const skipNextOpenMotionRef = useRef(false);\n  const restoreSuppressedAnimationRef = useRef<(() => void) | null>(null);\n  const [transitionStatus, setLocalTransitionStatus] =\n    useState<CollapsibleTransitionStatus>(context.open ? \"idle\" : undefined);\n  const statusRef = useRef(transitionStatus);\n  statusRef.current = transitionStatus;\n\n  const setTransitionStatus = useCallback(\n    (next: CollapsibleTransitionStatus) => {\n      statusRef.current = next;\n      setLocalTransitionStatus((current) => (current === next ? current : next));\n      setRootTransitionStatus(next);\n    },\n    [setRootTransitionStatus]\n  );\n\n  const suppressOpenAnimation = useCallback((panel: HTMLElement) => {\n    restoreSuppressedAnimationRef.current?.();\n    const priorValue = panel.style.getPropertyValue(\"animation-name\");\n    const priorPriority = panel.style.getPropertyPriority(\"animation-name\");\n    panel.style.setProperty(\"animation-name\", \"none\", \"important\");\n    restoreSuppressedAnimationRef.current = () => {\n      if (priorValue) {\n        panel.style.setProperty(\"animation-name\", priorValue, priorPriority);\n      } else {\n        panel.style.removeProperty(\"animation-name\");\n      }\n      restoreSuppressedAnimationRef.current = null;\n    };\n  }, []);\n\n  useHeidiLayoutEffect(() => {\n    setPanelIdOverride(id);\n    return () => {\n      setPanelIdOverride(undefined);\n    };\n  }, [id, setPanelIdOverride]);\n\n  useHeidiLayoutEffect(() => {\n    const panel = panelRef.current;\n    if (context.open && panel) {\n      suppressOpenAnimation(panel);\n    }\n    return () => {\n      restoreSuppressedAnimationRef.current?.();\n    };\n    // Effect teardown/recreation also prevents keyframe replay after Activity resumes.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  useHeidiLayoutEffect(() => {\n    if (context.open === previousOpenRef.current) {\n      return;\n    }\n    previousOpenRef.current = context.open;\n    const cycle = ++cycleRef.current;\n    const panel = panelRef.current;\n\n    if (context.open) {\n      if (panel) {\n        setPanelDimensions(panel, \"measured\");\n      }\n      if (skipNextOpenMotionRef.current) {\n        skipNextOpenMotionRef.current = false;\n        if (panel) {\n          suppressOpenAnimation(panel);\n          setPanelDimensions(panel, \"auto\");\n        }\n        setTransitionStatus(\"idle\");\n        return;\n      }\n      setTransitionStatus(\"starting\");\n      return;\n    }\n\n    restoreSuppressedAnimationRef.current?.();\n    if (panel) {\n      setPanelDimensions(panel, \"measured\");\n      panel.getBoundingClientRect();\n    }\n    const frame = requestAnimationFrame(() => {\n      if (cycle === cycleRef.current && !openRef.current) {\n        setTransitionStatus(\"ending\");\n      }\n    });\n    return () => {\n      cancelAnimationFrame(frame);\n    };\n  }, [context.open, setTransitionStatus, suppressOpenAnimation]);\n\n  useEffect(() => {\n    if (transitionStatus !== \"starting\") {\n      return;\n    }\n    const cycle = cycleRef.current;\n    const frame = requestAnimationFrame(() => {\n      if (cycle !== cycleRef.current || !openRef.current) {\n        return;\n      }\n      setTransitionStatus(\"idle\");\n      // Do not cancel this follow-up frame when `starting` becomes `idle`:\n      // that state change necessarily tears down this effect. The cycle/open\n      // guards make the callback safe across interruption and unmount.\n      requestAnimationFrame(() => {\n        const panel = panelRef.current;\n        if (!panel) {\n          return;\n        }\n        void waitForElementAnimations(panel).then(() => {\n          if (cycle === cycleRef.current && openRef.current) {\n            setPanelDimensions(panel, \"auto\");\n          }\n        });\n      });\n    });\n    return () => {\n      cancelAnimationFrame(frame);\n    };\n  }, [setTransitionStatus, transitionStatus]);\n\n  useEffect(() => {\n    if (transitionStatus !== \"ending\") {\n      return;\n    }\n    const cycle = cycleRef.current;\n    const frame = requestAnimationFrame(() => {\n      const panel = panelRef.current;\n      if (!panel) {\n        return;\n      }\n      void waitForElementAnimations(panel).then(() => {\n        if (cycle === cycleRef.current && !openRef.current) {\n          setPanelDimensions(panel, \"auto\");\n          setTransitionStatus(undefined);\n        }\n      });\n    });\n    return () => {\n      cancelAnimationFrame(frame);\n    };\n  }, [setTransitionStatus, transitionStatus]);\n\n  const shouldRender =\n    context.open ||\n    keepMounted ||\n    hiddenUntilFound ||\n    previousOpenRef.current ||\n    transitionStatus === \"ending\" ||\n    transitionStatus === \"starting\";\n  const hidden =\n    !context.open &&\n    !previousOpenRef.current &&\n    transitionStatus !== \"ending\";\n\n  useHeidiLayoutEffect(() => {\n    const panel = panelRef.current;\n    if (panel && hidden && hiddenUntilFound) {\n      panel.setAttribute(\"hidden\", \"until-found\");\n    }\n  }, [hidden, hiddenUntilFound, shouldRender]);\n\n  useEffect(() => {\n    const panel = panelRef.current;\n    if (!panel || !hiddenUntilFound) {\n      return;\n    }\n    const handleBeforeMatch = (event: Event) => {\n      const accepted = requestOpen(true, \"none\", event);\n      if (accepted) {\n        skipNextOpenMotionRef.current = true;\n      }\n    };\n    panel.addEventListener(\"beforematch\", handleBeforeMatch);\n    return () => {\n      panel.removeEventListener(\"beforematch\", handleBeforeMatch);\n    };\n  }, [hiddenUntilFound, requestOpen, shouldRender]);\n\n  useEffect(\n    () => () => {\n      cycleRef.current += 1;\n      setRootTransitionStatus(undefined);\n      restoreSuppressedAnimationRef.current?.();\n    },\n    [setRootTransitionStatus]\n  );\n\n  if (!shouldRender) {\n    return null;\n  }\n\n  const state: CollapsiblePanelState = {\n    disabled: context.disabled,\n    open: context.open,\n    transitionStatus\n  };\n  const resolvedId = id ?? context.defaultPanelId;\n  return renderHeidiElement({\n    className: COLLAPSIBLE_CLASSES.panel,\n    dataPart: \"collapsible-panel\",\n    element: \"div\",\n    props: {\n      ...nativeProps,\n      ...openStateAttributes(context.open),\n      ...transitionAttributes(transitionStatus),\n      children,\n      \"data-disabled\": context.disabled ? true : undefined,\n      \"data-state\": dataState(context.open),\n      hidden,\n      id: resolvedId,\n      ref: mergedRef\n    },\n    renderProps: { className, render, style },\n    state\n  });\n}\n\n/** @deprecated Use `CollapsiblePanel` / `Collapsible.Panel`. */\nexport const CollapsibleContent = CollapsiblePanel;\n/** @deprecated Use `CollapsiblePanelProps`. */\nexport type CollapsibleContentProps = CollapsiblePanelProps;\n\n/** Namespace sugar for client components. */\nexport const Collapsible = {\n  Content: CollapsibleContent,\n  Panel: CollapsiblePanel,\n  Root: CollapsibleRoot,\n  Trigger: CollapsibleTrigger\n} as const;\n",
      "path": "packages/heidi-ui/src/collapsible/collapsible.tsx",
      "target": "components/ui/heidi/collapsible/collapsible.tsx",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui collapsible — STRUCTURAL CSS only. No --hui-* theme values.\n * Closed default panels are absent from the DOM; retained panels use hidden.\n */\n\n@layer heidi-ui-base {\n  .hui-collapsible-root {\n    display: flex;\n    flex-direction: column;\n    min-inline-size: 0;\n  }\n\n  .hui-collapsible-trigger {\n    box-sizing: border-box;\n    min-block-size: 24px;\n    min-inline-size: 0;\n    overflow-wrap: anywhere;\n  }\n\n  .hui-collapsible-panel {\n    box-sizing: border-box;\n    min-inline-size: 0;\n    overflow-wrap: anywhere;\n  }\n\n  .hui-collapsible-panel[hidden]:not([hidden=\"until-found\"]) {\n    display: none !important;\n  }\n}\n",
      "path": "packages/heidi-ui/src/collapsible/collapsible.base.css",
      "target": "components/ui/heidi/collapsible/collapsible.base.css",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui collapsible — VISUAL theme (opt-in). The geometry follows the\n * Base UI 1.6 reference: a stable 12rem disclosure, 2rem trigger, and a\n * measured 150ms panel transition. Panel children own visual padding/borders.\n */\n\n@layer heidi-ui {\n  .hui-collapsible-root {\n    color: var(--hui-color-fg-default);\n    inline-size: min(12rem, 100%);\n    max-inline-size: 100%;\n  }\n\n  .hui-collapsible-trigger {\n    align-items: center;\n    background: var(--hui-color-bg-elevated);\n    block-size: calc(var(--hui-space-4) * 2);\n    border: var(--hui-border-width) var(--hui-border-style) var(--hui-color-border-default);\n    border-radius: 0;\n    color: var(--hui-color-fg-default);\n    cursor: pointer;\n    display: flex;\n    font: inherit;\n    font-size: var(--hui-text-body-size);\n    font-weight: var(--hui-font-weight-regular);\n    gap: var(--hui-space-2);\n    inline-size: 100%;\n    justify-content: space-between;\n    line-height: 1;\n    margin: 0;\n    padding-block: 0;\n    padding-inline: var(--hui-space-3) var(--hui-space-2);\n    text-align: start;\n    user-select: none;\n    white-space: normal;\n  }\n\n  @media (hover: hover) {\n    .hui-collapsible-trigger:hover:not([data-disabled=\"true\"]) {\n      background: var(--hui-color-interactive-ghost-bg-hover);\n    }\n  }\n\n  .hui-collapsible-trigger: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-collapsible-trigger > svg[aria-hidden=\"true\"] {\n    block-size: var(--hui-space-4);\n    display: block;\n    flex: 0 0 var(--hui-space-4);\n    inline-size: var(--hui-space-4);\n    transition: transform 100ms var(--hui-ease-out);\n  }\n\n  .hui-collapsible-trigger[data-panel-open=\"true\"] > svg[aria-hidden=\"true\"] {\n    transform: rotate(90deg);\n  }\n\n  [dir=\"rtl\"] .hui-collapsible-trigger > svg[aria-hidden=\"true\"] {\n    transform: scaleX(-1);\n  }\n\n  [dir=\"rtl\"]\n    .hui-collapsible-trigger[data-panel-open=\"true\"]\n    > svg[aria-hidden=\"true\"] {\n    transform: rotate(-90deg) scaleX(-1);\n  }\n\n  .hui-collapsible-trigger[data-disabled=\"true\"] {\n    border-color: var(--hui-color-border-default);\n    color: var(--hui-color-fg-muted);\n    cursor: not-allowed;\n  }\n\n  .hui-collapsible-panel {\n    block-size: var(--collapsible-panel-height, auto);\n    color: var(--hui-color-fg-muted);\n    display: flex;\n    flex-direction: column;\n    font-size: var(--hui-text-body-size);\n    line-height: var(--hui-text-body-leading);\n    transition: block-size var(--hui-duration-fast) var(--hui-ease-out);\n  }\n\n  .hui-collapsible-panel[data-open=\"true\"],\n  .hui-collapsible-panel[data-closed=\"true\"] {\n    overflow: hidden;\n  }\n\n  .hui-collapsible-panel[data-starting-style=\"true\"],\n  .hui-collapsible-panel[data-ending-style=\"true\"] {\n    block-size: 0;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .hui-collapsible-panel,\n    .hui-collapsible-trigger > svg[aria-hidden=\"true\"] {\n      transition-duration: 0s;\n    }\n  }\n\n  @media (forced-colors: active) {\n    .hui-collapsible-trigger {\n      background: Canvas;\n      border-color: ButtonText;\n      color: ButtonText;\n      forced-color-adjust: none;\n    }\n\n    .hui-collapsible-trigger:focus-visible {\n      outline-color: Highlight;\n    }\n\n    .hui-collapsible-trigger[data-disabled=\"true\"] {\n      border-color: GrayText;\n      color: GrayText;\n    }\n\n    .hui-collapsible-panel {\n      color: CanvasText;\n      transition-duration: 0s;\n    }\n  }\n}\n",
      "path": "packages/heidi-ui/src/collapsible/collapsible.theme.css",
      "target": "components/ui/heidi/collapsible/collapsible.theme.css",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui collapsible — aggregator (base + Heidi adapter + theme).\n */\n\n@import \"./collapsible.base.css\";\n@import \"../../../../styles/heidi-ui-adapter.css\";\n@import \"./collapsible.theme.css\";\n",
      "path": "packages/heidi-ui/src/collapsible/collapsible.css",
      "target": "components/ui/heidi/collapsible/collapsible.css",
      "type": "registry:ui"
    },
    {
      "content": "/** GENERATED from packages/heidi-ui/src/collapsible/collapsible.base.css + packages/heidi-ui/src/collapsible/collapsible.theme.css — do not edit by hand.\n * Rebuild: `bun run hui:build`. Freshness: `bun run contracts:check`. */\nexport const COLLAPSIBLE_CLASSES = {\n  panel: \"hui-collapsible-panel\",\n  root: \"hui-collapsible-root\",\n  trigger: \"hui-collapsible-trigger\",\n} as const;\n",
      "path": "packages/heidi-ui/src/collapsible/collapsible.classes.generated.ts",
      "target": "components/ui/heidi/collapsible/collapsible.classes.generated.ts",
      "type": "registry:ui"
    },
    {
      "content": "/** GENERATED from collapsible.anatomy.json + collapsible.base.css + collapsible.theme.css — do not edit by hand.\n * Rebuild: `bun run hui:build`. Freshness: `bun run contracts:check`. */\n\nexport type CollapsibleClosed = \"true\";\nexport type CollapsibleDisabled = \"true\";\nexport type CollapsibleEndingStyle = \"true\";\nexport type CollapsibleOpen = \"true\";\nexport type CollapsiblePanelOpen = \"true\";\nexport type CollapsibleStartingStyle = \"true\";\n\nexport const COLLAPSIBLE_ANATOMY = {\n  \"component\": \"collapsible\",\n  \"description\": \"Base-UI-shaped single disclosure: Root groups a named Trigger and a lazily mounted Panel. Supports authoritative controlled state, cancellable change details, focusable disabled triggers, non-button composition, keepMounted, hidden-until-found reveal, measured transition hooks, and full native host props. Use Accordion for coordinated multi-item disclosure.\",\n  \"schemaVersion\": 2,\n  \"parts\": {\n    \"panel\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-collapsible-panel\",\n      \"css\": {\n        \"structural\": [\n          \"box-sizing\",\n          \"min-inline-size\",\n          \"overflow-wrap\"\n        ]\n      },\n      \"dataPart\": \"collapsible-panel\",\n      \"description\": \"Lazily mounted disclosure panel. keepMounted uses hidden; hiddenUntilFound uses hidden=until-found and beforematch. Exposes measured size variables and starting/ending motion hooks without imposing landmark semantics.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"hiddenUntilFound\",\n        \"id\",\n        \"keepMounted\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-closed\": [\n          \"true\"\n        ],\n        \"data-ending-style\": [\n          \"true\"\n        ],\n        \"data-open\": [\n          \"true\"\n        ],\n        \"data-starting-style\": [\n          \"true\"\n        ]\n      }\n    },\n    \"root\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-collapsible-root\",\n      \"css\": {\n        \"structural\": [\n          \"display\",\n          \"flex-direction\",\n          \"min-inline-size\"\n        ]\n      },\n      \"dataPart\": \"collapsible-root\",\n      \"description\": \"Native-prop-forwarding state and layout container. Closed content is not rendered unless retention is explicitly requested.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"defaultOpen\",\n        \"disabled\",\n        \"onOpenChange\",\n        \"open\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {}\n    },\n    \"trigger\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-controls\",\n          \"aria-disabled\",\n          \"aria-expanded\"\n        ],\n        \"role\": null\n      },\n      \"class\": \"hui-collapsible-trigger\",\n      \"css\": {\n        \"structural\": [\n          \"box-sizing\",\n          \"min-block-size\",\n          \"min-inline-size\",\n          \"overflow-wrap\"\n        ]\n      },\n      \"dataPart\": \"collapsible-trigger\",\n      \"description\": \"Named disclosure button. aria-expanded always mirrors open; aria-controls and data-panel-open are present only while open. Disabled remains focusable and non-button render paths receive full keyboard semantics.\",\n      \"element\": \"button\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"disabled\",\n        \"nativeButton\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [\n        \":focus-visible\"\n      ],\n      \"states\": {\n        \"data-disabled\": [\n          \"true\"\n        ],\n        \"data-panel-open\": [\n          \"true\"\n        ]\n      }\n    }\n  },\n  \"rootProps\": [\n    \"defaultOpen\",\n    \"disabled\",\n    \"onOpenChange\",\n    \"open\"\n  ],\n  \"tokens\": [\n    \"--hui-border-style\",\n    \"--hui-border-width\",\n    \"--hui-color-bg-elevated\",\n    \"--hui-color-border-default\",\n    \"--hui-color-fg-default\",\n    \"--hui-color-fg-muted\",\n    \"--hui-color-focus-ring\",\n    \"--hui-color-interactive-ghost-bg-hover\",\n    \"--hui-duration-fast\",\n    \"--hui-ease-out\",\n    \"--hui-focus-ring-offset\",\n    \"--hui-focus-ring-width\",\n    \"--hui-font-weight-regular\",\n    \"--hui-space-2\",\n    \"--hui-space-3\",\n    \"--hui-space-4\",\n    \"--hui-text-body-leading\",\n    \"--hui-text-body-size\"\n  ]\n} as const;\n",
      "path": "packages/heidi-ui/src/collapsible/collapsible.anatomy.generated.ts",
      "target": "components/ui/heidi/collapsible/collapsible.anatomy.generated.ts",
      "type": "registry:ui"
    },
    {
      "content": "{\n  \"component\": \"collapsible\",\n  \"description\": \"Base-UI-shaped single disclosure: Root groups a named Trigger and a lazily mounted Panel. Supports authoritative controlled state, cancellable change details, focusable disabled triggers, non-button composition, keepMounted, hidden-until-found reveal, measured transition hooks, and full native host props. Use Accordion for coordinated multi-item disclosure.\",\n  \"schemaVersion\": 2,\n  \"parts\": {\n    \"panel\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-collapsible-panel\",\n      \"css\": {\n        \"structural\": [\n          \"box-sizing\",\n          \"min-inline-size\",\n          \"overflow-wrap\"\n        ]\n      },\n      \"dataPart\": \"collapsible-panel\",\n      \"description\": \"Lazily mounted disclosure panel. keepMounted uses hidden; hiddenUntilFound uses hidden=until-found and beforematch. Exposes measured size variables and starting/ending motion hooks without imposing landmark semantics.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"hiddenUntilFound\",\n        \"id\",\n        \"keepMounted\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-closed\": [\n          \"true\"\n        ],\n        \"data-ending-style\": [\n          \"true\"\n        ],\n        \"data-open\": [\n          \"true\"\n        ],\n        \"data-starting-style\": [\n          \"true\"\n        ]\n      }\n    },\n    \"root\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-collapsible-root\",\n      \"css\": {\n        \"structural\": [\n          \"display\",\n          \"flex-direction\",\n          \"min-inline-size\"\n        ]\n      },\n      \"dataPart\": \"collapsible-root\",\n      \"description\": \"Native-prop-forwarding state and layout container. Closed content is not rendered unless retention is explicitly requested.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"defaultOpen\",\n        \"disabled\",\n        \"onOpenChange\",\n        \"open\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {}\n    },\n    \"trigger\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-controls\",\n          \"aria-disabled\",\n          \"aria-expanded\"\n        ],\n        \"role\": null\n      },\n      \"class\": \"hui-collapsible-trigger\",\n      \"css\": {\n        \"structural\": [\n          \"box-sizing\",\n          \"min-block-size\",\n          \"min-inline-size\",\n          \"overflow-wrap\"\n        ]\n      },\n      \"dataPart\": \"collapsible-trigger\",\n      \"description\": \"Named disclosure button. aria-expanded always mirrors open; aria-controls and data-panel-open are present only while open. Disabled remains focusable and non-button render paths receive full keyboard semantics.\",\n      \"element\": \"button\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"disabled\",\n        \"nativeButton\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [\n        \":focus-visible\"\n      ],\n      \"states\": {\n        \"data-disabled\": [\"true\"],\n        \"data-panel-open\": [\"true\"]\n      }\n    }\n  },\n  \"rootProps\": [\n    \"defaultOpen\",\n    \"disabled\",\n    \"onOpenChange\",\n    \"open\"\n  ]\n}\n",
      "path": "packages/heidi-ui/src/collapsible/collapsible.anatomy.json",
      "target": "components/ui/heidi/collapsible/collapsible.anatomy.json",
      "type": "registry:ui"
    },
    {
      "content": "/**\n * Shared Web-Animations settling helpers (P6).\n *\n * Dialog, HoverCard and Collapsible each carried a byte-identical async\n * \"wait for this element's animations, but never forever\" routine, and all\n * four popover-family components carried their own `finiteAnimationDuration`.\n *\n * ponytail: the audit read this as one helper duplicated four times and\n * prescribed a single `waitForElementAnimations` covering \"the four call-site\n * variants\". Diffing them first — which that same entry insisted on — shows\n * that is not the shape. THREE are the same async routine differing only in\n * two flags. Menu's `afterPopoverAnimations` is a different algorithm: it is\n * callback-shaped rather than awaited, returns a canceller, subscribes to\n * `finish`/`cancel` events instead of racing `animation.finished`, and falls\n * back to computed CSS on engines without `getAnimations`. Forcing it into the\n * awaited shape would have rewritten working popover-dismissal logic to make a\n * count go from four to one. So Menu keeps its own routine and shares only the\n * duration helper it genuinely had in common.\n *\n * The duration helper resolves to MENU's copy, which was the strict one: the\n * other three returned `timing.endTime` raw, so a non-finite or negative\n * `endTime` (reachable via a negative `endDelay`) propagated into\n * `setTimeout` — and `setTimeout(fn, NaN)` fires at 0ms, collapsing the wait\n * it was supposed to bound. Same class as the non-finite bounds that reached\n * Slider's and Progress's ARIA.\n */\n\n/** Ceiling on any settle wait. A stuck animation must not strand a component. */\nexport const MAX_ANIMATION_WAIT_MS = 30_000;\n\n/** Grace added to the longest observed animation before the wait gives up. */\nexport const ANIMATION_WAIT_GRACE_MS = 250;\n\nexport function finiteAnimationDuration(\n  animation: Animation\n): number | undefined {\n  const timing = animation.effect?.getComputedTiming();\n  if (\n    !timing ||\n    timing.iterations === Infinity ||\n    typeof timing.endTime !== \"number\" ||\n    !Number.isFinite(timing.endTime)\n  ) {\n    return undefined;\n  }\n  return Math.max(0, timing.endTime);\n}\n\nexport async function afterNextPaint(): Promise<void> {\n  await new Promise<void>((resolve) => {\n    requestAnimationFrame(() => requestAnimationFrame(() => resolve()));\n  });\n}\n\n/** Animations worth waiting on: real, still running, and finitely long. */\nexport function pendingAnimations(\n  element: Element,\n  subtree: boolean\n): Array<{ animation: Animation; duration: number }> {\n  // Embedded and older engines can omit the Web Animations inspection API.\n  // In that no-observer path the state must still settle instead of throwing.\n  if (typeof element.getAnimations !== \"function\") {\n    return [];\n  }\n  return element\n    .getAnimations(subtree ? { subtree: true } : undefined)\n    .map((animation) => ({\n      animation,\n      duration: finiteAnimationDuration(animation)\n    }))\n    .filter(\n      (entry): entry is { animation: Animation; duration: number } =>\n        entry.duration !== undefined &&\n        entry.duration > 0 &&\n        entry.animation.playState !== \"finished\" &&\n        entry.animation.playState !== \"idle\"\n    );\n}\n\nexport type WaitForElementAnimationsOptions = {\n  /**\n   * Wait two frames before inspecting. Dialog and HoverCard need this: they\n   * ask immediately after a state flip, before the engine has started the\n   * animations they mean to wait for. Collapsible asks after the fact and\n   * must NOT gain the extra frames — that would be an observable timing\n   * change, and this slice does not make those.\n   */\n  awaitPaint?: boolean;\n  /** Inspect descendants too — for panels whose motion lives on children. */\n  subtree?: boolean;\n};\n\nexport async function waitForElementAnimations(\n  element: Element,\n  { awaitPaint = false, subtree = false }: WaitForElementAnimationsOptions = {}\n): Promise<void> {\n  if (awaitPaint) {\n    await afterNextPaint();\n    // ponytail: the connectedness check is deliberately INSIDE the awaitPaint\n    // branch rather than unconditional. It is not a general safety net — it\n    // exists because those two frames are a window in which the element can be\n    // torn down, and it was present in exactly the two copies that wait. The\n    // non-waiting caller has no such window: nothing can run between its own\n    // check and this call, so hoisting the guard would be adding behaviour to\n    // Collapsible in a slice whose whole premise is changing none.\n    if (!element.isConnected) {\n      return;\n    }\n  }\n  const animations = pendingAnimations(element, subtree);\n  if (animations.length === 0) {\n    return;\n  }\n  const maximum = Math.min(\n    MAX_ANIMATION_WAIT_MS,\n    Math.max(...animations.map(({ duration }) => duration)) +\n      ANIMATION_WAIT_GRACE_MS\n  );\n  let timeout = 0;\n  await Promise.race([\n    Promise.allSettled(animations.map(({ animation }) => animation.finished)),\n    new Promise<void>((resolve) => {\n      timeout = window.setTimeout(resolve, maximum);\n    })\n  ]);\n  window.clearTimeout(timeout);\n}\n",
      "path": "packages/heidi-ui/src/_internal/animation-wait.ts",
      "target": "components/ui/heidi/_internal/animation-wait.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 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": "collapsible",
  "registryDependencies": [
    "@heidi-ui/heidi-tokens"
  ],
  "title": "Collapsible",
  "type": "registry:ui"
}
