{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-auto-height",
  "title": "Auto Height",
  "description": "The height:auto spring primitive: a wrapper whose height springs to its content, opens and closes with the content fading in after the height lands, and rests at height:auto with no clipping.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/auto-height/auto-height.tsx",
      "content": "\"use client\";\n\nimport { animate, type AnimationPlaybackControlsWithThen, type Transition } from \"motion/react\";\nimport { useLayoutEffect, useRef, useState, type CSSProperties, type HTMLAttributes, type ReactNode } from \"react\";\nimport { duration, ease, exitFast, reducedFade, smooth, useSafeReducedMotion } from \"../../lib/motion\";\n\nexport type AutoHeightProps = Omit<HTMLAttributes<HTMLDivElement>, \"children\"> & {\n  children: ReactNode;\n  /**\n   * `false` collapses to 0 and hides the content (`visibility: hidden`, so it leaves the tab\n   * order and the accessibility tree once closed; `inert` while closing). Default `true`.\n   */\n  open?: boolean;\n  /** Fade the content in once the height is mostly there, and out before it closes. Default `true`. */\n  fade?: boolean;\n  /** The height spring. Default `smooth`: no overshoot, so content never bounces past its edge. */\n  transition?: Transition;\n  /** Classes for the measured inner wrapper. Put padding here, never on the root. */\n  innerClassName?: string;\n  /** Fires when an open or close has fully settled. */\n  onOpenChangeComplete?: (open: boolean) => void;\n};\n\n/** Start the content fade this far into the height spring, so it lands just after the height does. */\nconst FADE_DELAY = 0.12;\n\n/**\n * Animates its own height to whatever its content measures: a `height: auto` spring.\n * Content that grows or shrinks while open springs to its new height (a list filtering, an\n * error appearing); `open` collapses it to 0 and back. At rest the root is `height: auto`\n * with no clipping, so focus rings and shadows inside are never cut off; it clips the\n * vertical axis only while moving. Interruptible: a change mid-flight continues from the\n * current height. Reduced motion snaps the height and fades opacity only.\n */\nexport function AutoHeight({\n  open = true,\n  fade = true,\n  transition = smooth,\n  className = \"\",\n  innerClassName = \"\",\n  style,\n  onOpenChangeComplete,\n  children,\n  ...props\n}: AutoHeightProps) {\n  const outerRef = useRef<HTMLDivElement>(null);\n  const innerRef = useRef<HTMLDivElement>(null);\n  const reduced = useSafeReducedMotion();\n  // Styles that describe the first frame only; afterwards the element is driven imperatively.\n  const [initialOpen] = useState(open);\n  const prevOpen = useRef(open);\n  const openRef = useRef(open);\n  const contentHeight = useRef<number | null>(null);\n  const heightAnim = useRef<AnimationPlaybackControlsWithThen | null>(null);\n  const run = useRef(0);\n  const latest = useRef({ reduced, transition, fade, onOpenChangeComplete });\n\n  useLayoutEffect(() => {\n    latest.current = { reduced, transition, fade, onOpenChangeComplete };\n  });\n\n  /** Spring the root from `from` to `to` px, then release it to `auto` (open) or keep it at 0 (closed). */\n  const springHeight = (from: number, to: number, settledOpen: boolean | null) => {\n    const el = outerRef.current;\n    const content = innerRef.current;\n    if (!el || !content) return;\n    const { reduced, transition, onOpenChangeComplete } = latest.current;\n    const token = ++run.current;\n    heightAnim.current?.stop();\n    el.style.overflowY = \"clip\";\n    el.style.height = `${from}px`;\n    const settle = () => {\n      if (token !== run.current) return;\n      heightAnim.current = null;\n      if (openRef.current) {\n        el.style.height = \"\";\n        el.style.overflowY = \"\";\n      } else {\n        el.style.height = \"0px\";\n        content.style.visibility = \"hidden\";\n      }\n      if (settledOpen !== null) onOpenChangeComplete?.(settledOpen);\n    };\n    if (reduced || Math.abs(from - to) < 0.5) {\n      el.style.height = `${to}px`;\n      settle();\n      return;\n    }\n    heightAnim.current = animate(el, { height: [from, to] }, transition);\n    heightAnim.current.then(settle);\n  };\n\n  /** The root's rendered height right now: mid-animation it is whatever the spring has reached. */\n  const currentHeight = () => {\n    const el = outerRef.current!;\n    return el.style.height && el.style.height !== \"auto\" ? parseFloat(getComputedStyle(el).height) : el.offsetHeight;\n  };\n\n  // Open and close.\n  useLayoutEffect(() => {\n    if (prevOpen.current === open) return;\n    prevOpen.current = open;\n    openRef.current = open;\n    const el = outerRef.current;\n    const content = innerRef.current;\n    if (!el || !content) return;\n    const { reduced, fade } = latest.current;\n    const from = heightAnim.current ? currentHeight() : open ? 0 : el.offsetHeight;\n    if (open) {\n      content.style.visibility = \"\";\n      const to = content.offsetHeight;\n      contentHeight.current = to;\n      if (fade) {\n        animate(\n          content,\n          { opacity: [Number(getComputedStyle(content).opacity), 1] },\n          reduced ? reducedFade : { duration: duration.standard, ease: ease.enter, delay: FADE_DELAY },\n        );\n      } else content.style.opacity = \"\";\n      springHeight(from, to, true);\n    } else {\n      if (fade) animate(content, { opacity: 0 }, reduced ? reducedFade : exitFast);\n      springHeight(from, 0, false);\n    }\n  }, [open]);\n\n  // Content resizing while open.\n  useLayoutEffect(() => {\n    const content = innerRef.current;\n    if (!content) return;\n    contentHeight.current = content.offsetHeight;\n    const observer = new ResizeObserver(() => {\n      const next = content.offsetHeight;\n      const prev = contentHeight.current;\n      contentHeight.current = next;\n      if (!openRef.current || prev === null || prev === next) return;\n      // At rest the root is already `auto` (it has jumped this frame, before paint): start from the old height.\n      springHeight(heightAnim.current ? currentHeight() : prev, next, null);\n    });\n    observer.observe(content);\n    return () => observer.disconnect();\n  }, []);\n\n  const rootStyle: CSSProperties = initialOpen ? { ...style } : { height: 0, overflowY: \"clip\", ...style };\n  const innerStyle: CSSProperties | undefined = initialOpen ? undefined : { visibility: \"hidden\", opacity: fade ? 0 : undefined };\n\n  return (\n    <div ref={outerRef} className={className} style={rootStyle} {...props}>\n      <div ref={innerRef} inert={!open} className={`flow-root ${innerClassName}`} style={innerStyle}>\n        {children}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/auto-height/auto-height.tsx"
    }
  ],
  "type": "registry:ui"
}