{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-auto-height",
  "type": "registry:component",
  "title": "Auto height",
  "description": "A reveal that grows to its content's measured height",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-motion.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/components/src/auto-height.tsx",
      "target": "@ui/gpuixui/components/auto-height.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props, PublicInstance } from \"@gpuix/react\";\nimport { useGpuix } from \"@gpuix/react\";\nimport {\n  mergeStyles,\n  resolveStyle,\n  type StateStyle,\n} from \"@gpuix/react/floating\";\nimport type { MotionLevel } from \"../motion/timing.js\";\nimport { cn } from \"../tw/index.js\";\nimport * as Option from \"effect/Option\";\nimport {\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { measuredHeight } from \"./internal/measure.js\";\nimport {\n  PresenceShield,\n  delayedTiming,\n  presenceMotion,\n  presenceStyle,\n  useMotionPreference,\n  useSurfacePresence,\n  useSurfaceTiming,\n  type SurfaceTiming,\n} from \"./internal/presence.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport { useTheme } from \"./internal/theme.js\";\n\nconst FADE_LEAD_MS = 20;\n\nexport interface AutoHeightState {\n  readonly height: number;\n\n  readonly open: boolean;\n\n  readonly reducedMotion: boolean;\n}\n\nexport interface AutoHeightProps extends Omit<Props, \"children\" | \"style\"> {\n  readonly open: boolean;\n  readonly children?: ReactNode;\n\n  readonly level?: MotionLevel;\n\n  readonly timing?: SurfaceTiming;\n\n  readonly className?: string;\n\n  readonly contentClassName?: string;\n\n  readonly style?: StateStyle<AutoHeightState>;\n}\n\nexport const AutoHeight = ({\n  open,\n  level = \"normal\",\n  timing: ownTiming,\n  className,\n  contentClassName,\n  testId,\n  style,\n  children,\n  ...rest\n}: AutoHeightProps) => {\n  const theme = useTheme();\n  const renderer = useGpuix().renderer;\n  const preference = useMotionPreference();\n  const levelTiming = useSurfaceTiming(level);\n  const timing = ownTiming ?? levelTiming;\n  const presence = useSurfacePresence(open, timing);\n  const [measured, setMeasured] = useState(Option.none<number>());\n  const content = useRef<PublicInstance | null>(null);\n\n  const fade = {\n    enter: delayedTiming(timing.enter, FADE_LEAD_MS),\n    exit: timing.exit,\n  };\n\n  useEffect(() => {\n    if (presence.mounted) return;\n    setMeasured((current) =>\n      Option.isSome(current) ? Option.none() : current,\n    );\n  }, [presence.mounted]);\n\n  useLayoutEffect(() => {\n    if (!presence.mounted) return;\n    const instance = content.current;\n    if (instance === null) return;\n    const bounds = renderer?.getElementBounds?.(instance.id) ?? null;\n    if (bounds === null) return;\n    setMeasured((current) =>\n      Option.isSome(current) ? current : Option.some(bounds.height),\n    );\n  }, [presence.mounted, renderer]);\n\n  if (!presence.mounted) return null;\n\n  const height = Option.getOrElse(measured, () => 0);\n  const state: AutoHeightState = {\n    height,\n    open,\n    reducedMotion: preference === \"reduced\",\n  };\n\n  return (\n    <div\n      {...rest}\n      testId={testId}\n      motion={presenceMotion(\n        {\n          initial: { height: 0 },\n          animate: { height: open ? height : 0 },\n          exit: { height: 0 },\n        },\n        presence,\n      )}\n      style={mergeStyles(\n        mergeStyles(\n          {\n            width: \"100%\",\n            overflow: \"hidden\",\n\n            position: \"relative\",\n            ...presenceStyle(presence.present, \"wrapper\"),\n          },\n          recipeStyle(cn(className), theme),\n        ),\n        resolveStyle(style, state),\n      )}\n    >\n      <div\n        ref={content}\n        testId={testId === undefined ? undefined : `${testId}-measured`}\n        motion={presenceMotion(\n          {\n            initial: { opacity: 0 },\n            animate: { opacity: 1 },\n            exit: { opacity: 0 },\n          },\n          presence,\n          fade,\n        )}\n        onLayout={(event) => {\n          Option.match(measuredHeight(event), {\n            onNone: () => undefined,\n            onSome: (next) => setMeasured(Option.some(next)),\n          });\n        }}\n        style={recipeStyle(cn(contentClassName), theme)}\n      >\n        {children}\n      </div>\n      <PresenceShield present={presence.present} />\n    </div>\n  );\n};\n\nexport const autoHeightMeta = {\n  name: \"Auto height\",\n  tier: 5,\n  status: \"done\",\n  gpuixSupport: \"yes\",\n  limitations: [\n    \"the first measurement is read through the renderer's optional `getElementBounds`; a host without it starts the growth on the `onLayout` report a frame later\",\n    \"the clipped box does not reflow its content: a percentage height inside it measures against the content box\",\n    \"the reveal unmounts its subtree after the collapse, so state inside it does not survive a close and the next open measures the body again — a measurement is retired with the subtree it came from, never reused as a stale target\",\n  ],\n  docs: \"docs/auto-height.mdx\",\n  example: \"examples/auto-height.example.tsx\",\n  testFile: \"src/behaviour/auto-height.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/components/src/internal/measure.ts",
      "target": "@ui/gpuixui/components/internal/measure.ts",
      "type": "registry:lib",
      "content": "import type { EventPayload } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\n\nexport interface MeasuredBox {\n  readonly x: number;\n  readonly y: number;\n  readonly width: number;\n  readonly height: number;\n}\n\nconst measurable = (value: number | undefined): Option.Option<number> =>\n  Option.filter(Option.fromNullishOr(value), Number.isFinite);\n\nexport const measuredBox = (event: EventPayload): Option.Option<MeasuredBox> =>\n  Option.all({\n    x: measurable(event.x),\n    y: measurable(event.y),\n    width: measurable(event.width),\n    height: measurable(event.height),\n  });\n\nexport const measuredHeight = (event: EventPayload): Option.Option<number> =>\n  measurable(event.height);\n\nexport interface MeasuredPoint {\n  readonly x: number;\n  readonly y: number;\n}\n\nexport const measuredPoint = (\n  event: EventPayload,\n): Option.Option<MeasuredPoint> =>\n  Option.all({\n    x: measurable(event.x),\n    y: measurable(event.y),\n  });\n"
    },
    {
      "path": "packages/components/src/internal/presence.tsx",
      "target": "@ui/gpuixui/components/internal/presence.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { MotionProps } from \"@gpuix/react\";\nimport {\n  delayedTiming,\n  exitWindowMs,\n  fadeFrom,\n  fadeFromAnchor,\n  fadeFromSide,\n  fadeOnly,\n  presenceMotion as motionOfPresence,\n  presenceStyle,\n  transitionWindowMs,\n  type PresenceFrames,\n  type PresenceHitRule,\n  type PresenceSide,\n  type PresenceTiming,\n  type PresenceTransition,\n} from \"../../motion/presence.js\";\nimport {\n  MotionProvider,\n  useMotionPreference,\n  useSurfacePresence,\n  useSurfaceTiming,\n  type MotionProviderProps,\n  type SurfacePresence,\n} from \"../../motion/react/presence.js\";\nimport {\n  motionTiming,\n  type MotionLevel,\n  type MotionPreference,\n} from \"../../motion/timing.js\";\n\nexport { MotionProvider, useMotionPreference, type MotionProviderProps };\n\nexport type SurfaceFrames = PresenceFrames;\n\nexport type SurfaceTiming = PresenceTiming;\n\nexport type PanelSide = PresenceSide;\n\nexport {\n  delayedTiming,\n  exitWindowMs,\n  fadeFrom,\n  fadeFromAnchor,\n  fadeFromSide,\n  fadeOnly,\n  presenceStyle,\n  transitionWindowMs,\n};\nexport type { PresenceHitRule };\n\nexport { useSurfacePresence, useSurfaceTiming, type SurfacePresence };\n\nexport const moveTiming = (\n  level: MotionLevel,\n  preference: MotionPreference,\n): PresenceTransition => motionTiming(level, { phase: \"standard\", preference });\n\nexport const useMoveTiming = (level: MotionLevel): PresenceTransition =>\n  moveTiming(level, useMotionPreference());\n\nexport const presenceMotion = (\n  frames: SurfaceFrames,\n  presence: SurfacePresence,\n  timing: SurfaceTiming = presence.timing,\n): MotionProps => motionOfPresence(frames, presence.present, timing);\n\nexport const PresenceShield = ({ present }: { readonly present: boolean }) =>\n  present ? null : (\n    <div\n      testId=\"presence-shield\"\n      style={{ position: \"absolute\", top: 0, right: 0, bottom: 0, left: 0 }}\n    />\n  );\n\nexport const presenceTabStop = (\n  present: boolean,\n): { readonly tabIndex?: number } => (present ? {} : { tabIndex: -1 });\n"
    },
    {
      "path": "packages/components/src/internal/recipe.ts",
      "target": "@ui/gpuixui/components/internal/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(512);\n"
    },
    {
      "path": "packages/components/src/internal/theme.ts",
      "target": "@ui/gpuixui/components/internal/theme.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport * as Schema from \"effect/Schema\";\nimport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  brandTextOn,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  pointerWash,\n  selectionOn,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n  type BoxShadow,\n  type ElevationLevel,\n  type FindHighlightSource,\n  type NativeTextFill,\n  type PlatformName,\n  type PointerWashSurface,\n  type StatusTone,\n  type SyntaxColors,\n  type SyntaxRole,\n  type TextRole,\n  type Theme,\n  type ThemeColors,\n} from \"../../tokens/index.js\";\n\nexport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n};\nexport type {\n  BoxShadow,\n  ElevationLevel,\n  FindHighlightSource,\n  NativeTextFill,\n  PlatformName,\n  StatusTone,\n  SyntaxColors,\n  SyntaxRole,\n  TextRole,\n  Theme,\n};\nexport { ThemeProvider, useTheme } from \"../../tokens/react.js\";\n\nexport const keyboardRing = (theme: Theme, invalid = false): StyleDesc =>\n  controlFocusRing(theme, { invalid }) ?? {};\n\nexport interface ControlRingState {\n  readonly disabled?: boolean;\n\n  readonly invalid?: boolean;\n}\n\nexport const controlRing = (\n  theme: Theme,\n  state: ControlRingState = {},\n): StyleDesc => controlFocusRing(theme, state) ?? {};\n\nexport const pressWash = (theme: Theme, replaced: PointerWashSurface): string =>\n  pointerWash(theme, \"active\", replaced) ?? theme.colors.active;\n\nexport const selectionWash = (theme: Theme): StyleDesc => ({\n  selectionColor: theme.colors.selection,\n});\n\nexport const keepContent: StyleDesc = { keyboardFocusDim: false };\n\nexport const disabledFade = (\n  disabled: boolean,\n): { readonly opacity: number } | undefined =>\n  disabled ? { opacity: disabledOpacity } : undefined;\n\nconst Tones = Schema.Literals([\"info\", \"success\", \"warning\", \"danger\"]);\n\nexport type Tone = typeof Tones.Type;\n\nexport const toneOf = (value: unknown): Tone =>\n  Schema.is(Tones)(value) ? value : \"info\";\n\nexport interface ToneStyle {\n  readonly fg: string;\n  readonly bg: string;\n  readonly border: string;\n}\n\nconst TOKEN_TONE: Readonly<Record<Tone, StatusTone>> = {\n  info: \"info\",\n  success: \"success\",\n  warning: \"warning\",\n  danger: \"destructive\",\n};\n\nexport const tone = (theme: Theme, value: Tone): ToneStyle => {\n  const { background, border, foreground } = statusTone(\n    theme,\n    TOKEN_TONE[value],\n  );\n  return { bg: background, border, fg: foreground };\n};\n\nconst softRoles: Readonly<Record<Tone, keyof ThemeColors>> = {\n  info: \"infoSoft\",\n  success: \"successSoft\",\n  warning: \"warningSoft\",\n  danger: \"destructiveSoft\",\n};\n\nexport const messageLink = (theme: Theme, value: Tone): string =>\n  brandTextOn(theme, softRoles[value]);\n\nexport const messageSelection = (theme: Theme, value: Tone): string =>\n  selectionOn(theme, softRoles[value]);\n\nexport const toneSurface = (theme: Theme, value: Tone): StyleDesc => {\n  const { bg, border, fg } = tone(theme, value);\n  return {\n    backgroundColor: bg,\n    color: fg,\n    borderWidth: theme.borderWidth.thin,\n    borderColor: border,\n  };\n};\n"
    }
  ],
  "docs": "[Auto height](https://gpuixui.sudokw.com/components/auto-height) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/components/auto-height",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}