{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "transition-panel",
  "title": "Transition Panel",
  "description": "A transition-panel component.",
  "registryDependencies": [
    "@cubby-ui/use-animated-height"
  ],
  "files": [
    {
      "path": "registry/default/transition-panel/transition-panel.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { useAnimatedHeight } from \"@/registry/default/hooks/use-animated-height\";\nimport { cn } from \"@/lib/utils\";\n\ntype TransitionPanelInitialFocus =\n  | boolean\n  | React.RefObject<HTMLElement | null>;\n\ntype TransitionPanelProps = useRender.ComponentProps<\"div\"> & {\n  /** viewKey of the visible view. Must match a `<TransitionPanelView>` descendant. */\n  activeKey: string;\n  /**\n   * Transition style (default `\"slide\"`):\n   *  - `\"slide\"`: direction-aware horizontal slide (forward from the right,\n   *    back from the left).\n   *  - `\"fade\"`: non-directional crossfade with a subtle scale from `0.96` to\n   *    `1`, duration adapting to the height change via `--tp-fade-duration`.\n   */\n  transition?: \"slide\" | \"fade\";\n};\n\ntype TransitionPanelViewProps = useRender.ComponentProps<\"div\"> & {\n  /** Matched against the parent's `activeKey` to determine visibility. */\n  viewKey: string;\n  /**\n   * What to focus when this view becomes active after a swap (Base UI's\n   * `initialFocus` convention):\n   *  - `true` (default): first tabbable element inside the view\n   *  - `false`: don't move focus\n   *  - `RefObject<HTMLElement>`: that specific element\n   *\n   * Skipped on initial render (the parent owns first-mount focus).\n   */\n  initialFocus?: TransitionPanelInitialFocus;\n};\n\nconst SLIDE_DISTANCE = \"18%\";\n\n// Tabbable candidate set, mirroring Base UI. Skips niche cases (iframe, object,\n// media with controls); pass an explicit `initialFocus` ref for those. Includes\n// `[contenteditable]` for rich-text surfaces, and excludes hidden inputs.\nconst FOCUSABLE_SELECTOR =\n  'a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), summary, [tabindex]:not([tabindex=\"-1\"]), [contenteditable]:not([contenteditable=\"false\"])';\n\ntype ViewEntry = {\n  el: HTMLElement;\n  initialFocus: TransitionPanelInitialFocus;\n};\n\ntype TransitionPanelContextValue = {\n  activeKey: string;\n  transition: \"slide\" | \"fade\";\n  enterFrom: string;\n  exitTo: string;\n  mounted: boolean;\n  registerView: (\n    key: string,\n    el: HTMLElement,\n    initialFocus: TransitionPanelInitialFocus,\n  ) => () => void;\n};\n\nconst TransitionPanelContext =\n  React.createContext<TransitionPanelContextValue | null>(null);\n\n// Does the browser defer `display: block` to `none` until the exit transition\n// finishes (via `transition-behavior: allow-discrete`)? Chrome and Safari do.\n// Firefox parses `allow-discrete` (it shows up in computed style) but still\n// flips `display:none` instantly, killing the exit animation. There's no\n// `@supports` query that catches this (Firefox claims support), so we sniff the\n// UA. When this returns false the exit runs through a JS deferred-hide instead\n// (see `TransitionPanelView`). Entering works everywhere regardless: it uses\n// `@starting-style`, not the discrete exit.\nfunction supportsDiscreteDisplayExit(): boolean {\n  if (typeof navigator === \"undefined\") return true; // SSR: assume the CSS path\n  return !/firefox/i.test(navigator.userAgent);\n}\n\nfunction useCssExitSupported(): boolean {\n  // Start as `true` so SSR and the first client render agree (both take the CSS\n  // path, and inactive views are `display:none` either way so nothing flashes).\n  // Correct it after mount, which happens before any swap can fire.\n  const [supported, setSupported] = React.useState(true);\n  React.useEffect(() => {\n    setSupported(supportsDiscreteDisplayExit());\n  }, []);\n  return supported;\n}\n\n// Why CSS instead of Motion: the height animation drives the real `height`\n// property (layout per frame), which is fine for the typical isolated, brief,\n// small-area panel swap. A Motion `layout` approach animates via transform\n// (compositor only) but adds ~25-30kb to every consumer plus FLIP edge cases on\n// rapid swaps. Not worth it as a design-system default. Reach for Motion in a\n// fork if a high-frequency or large-surface case ever proves it's needed.\n\n/**\n * Animated swap between N named views, in two styles: `slide` (default,\n * direction-aware horizontal slide) and `fade` (non-directional crossfade with\n * a subtle scale from `0.96` to `1`). Pure CSS, no Motion: `@starting-style`,\n * `transition-discrete` for the `display` swap, and CSS custom properties.\n * Height animates via `useAnimatedHeight`, gated to swaps only so content that\n * animates its own height isn't fought by a competing tween.\n *\n * Compound component: `TransitionPanelView` children share state through\n * `TransitionPanelContext` and register themselves via `registerView`. Views\n * needn't be direct children. HOCs, fragments, Suspense, and conditional or\n * mapped renderings all route through context, and registration re-derives view\n * order from DOM position so a remounted view keeps its source-order slot.\n *\n * Both `TransitionPanel` and `TransitionPanelView` accept any `<div>` prop, a\n * composed `ref`, and a Base UI `render` prop (e.g. render a view as a `Card`).\n *\n * Data attributes (Base UI conventions):\n *   - Root: `data-transition=\"slide\" | \"fade\"`,\n *           `data-activation-direction=\"left\" | \"right\" | \"none\"`\n *   - View: `data-active` (presence when active), `data-viewkey=\"...\"`\n *\n * CSS custom properties (set on root, inherited by views; overridable via the\n * `style` prop or a CSS rule):\n *   - `--tp-duration` (default `240ms`): height plus slide/opacity duration.\n *   - `--tp-fade-duration`: crossfade duration in `fade` mode. Defaults to the\n *     size-adaptive `--fade-duration` the height observer writes; set a fixed\n *     value to opt out of the adaptive crossfade. (Don't set `--fade-duration`\n *     yourself, it's the observer-written source for this default.)\n *   - `--tp-ease` / `--tp-fade-ease`: easing for height/slide and crossfade.\n *\n * Browser support: the entrance animation needs `@starting-style` (Chrome 117+,\n * Safari 17.4+, Firefox 129+); older browsers degrade to an instant, motionless\n * swap. The exit defers `display:none` via `transition-behavior: allow-discrete`\n * where the browser supports it (Chrome/Safari). Firefox doesn't defer it on the\n * way out, so there the exit falls back to a JS-deferred hide (see\n * `TransitionPanelView`). That keeps the exit animation in every engine while\n * Chrome/Safari stay pure-CSS at runtime.\n *\n * Usage:\n * ```tsx\n * <TransitionPanel activeKey={step} transition=\"fade\">\n *   <TransitionPanelView viewKey=\"list\">...</TransitionPanelView>\n *   <TransitionPanelView viewKey=\"create\">...</TransitionPanelView>\n * </TransitionPanel>\n * ```\n */\nfunction TransitionPanel({\n  activeKey,\n  transition = \"slide\",\n  render,\n  className,\n  style,\n  children,\n  ...props\n}: TransitionPanelProps) {\n  // Swap height animation state. Declared up here (ahead of the narrative\n  // order) because `handleContentResize` below has to exist before\n  // `useAnimatedHeight` is called, to be passed in as its resize callback.\n  const heightState = React.useRef<HeightAnimationState>({\n    animation: null,\n    target: null,\n  });\n\n  // Retarget a swap animation whose destination moved. The observer fires on\n  // every content resize; while a swap animation is in flight, one means the\n  // height we're heading toward is already stale (an image decoded, async data\n  // landed, a nested collapsible opened). Restart from the mid-flight height\n  // toward the new one — the same retarget a CSS transition on `height` would\n  // have done — instead of finishing at the old target and snapping.\n  //\n  // Outside a swap there's no running animation, so this no-ops and the\n  // observer's instant write stands. That's the whole point of the gate:\n  // content animating its own height still drives a single tween the panel\n  // tracks frame-for-frame, with no competing tween of ours.\n  const handleContentResize = React.useCallback(\n    (height: number, outer: HTMLElement) => {\n      const state = heightState.current;\n      if (state.animation?.playState !== \"running\") return;\n      // The swap's own settle lands here too (the observer fires right after\n      // the layout effect starts the animation); only a moved target retargets.\n      // Rounded/tolerant because the observer reports a fractional border-box\n      // size while the target came from integer `offsetHeight`.\n      if (state.target !== null && Math.abs(state.target - height) < 1) return;\n      animateHeight(outer, Math.round(height), state);\n    },\n    [],\n  );\n\n  const { outerRef, innerRef } = useAnimatedHeight(handleContentResize);\n\n  // Shadow useAnimatedHeight's inner callback ref with a RefObject we can read\n  // in `registerView` (for the DOM-order rebuild), still forwarding the node on.\n  const innerDivRef = React.useRef<HTMLDivElement | null>(null);\n  const setInnerRef = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      innerDivRef.current = node;\n      innerRef(node);\n    },\n    [innerRef],\n  );\n\n  // View registry: the ref holds DOM elements + focus contracts (read only in\n  // effects), while `orderedKeys` mirrors key order as state so render-time\n  // logic (direction calc, dev warning) stays reactive and lint-clean.\n  const viewsRef = React.useRef<Map<string, ViewEntry>>(new Map());\n  const [orderedKeys, setOrderedKeys] = React.useState<string[]>([]);\n\n  // Re-derive ordering from DOM position on each registration. A view that\n  // unmounts and remounts would otherwise reinsert at the end of the Map and\n  // break direction calc; `[data-viewkey]` in document order is React's source\n  // order regardless of HOC / fragment / Suspense wrapping.\n  const registerView = React.useCallback<\n    TransitionPanelContextValue[\"registerView\"]\n  >((key, el, initialFocus) => {\n    const inner = innerDivRef.current;\n    if (inner) {\n      const wrappers = Array.from(\n        inner.querySelectorAll<HTMLElement>(\"[data-viewkey]\"),\n      );\n      const next = new Map<string, ViewEntry>();\n      for (const node of wrappers) {\n        const k = node.dataset.viewkey;\n        if (!k) continue;\n        if (k === key) {\n          next.set(k, { el, initialFocus });\n        } else {\n          const existing = viewsRef.current.get(k);\n          if (existing) next.set(k, existing);\n        }\n      }\n      viewsRef.current = next;\n    } else {\n      // innerDivRef is set before view layout effects run, so this shouldn't\n      // fire in practice; fall back to insertion-order Map.set if it does.\n      viewsRef.current.set(key, { el, initialFocus });\n    }\n    setOrderedKeys(Array.from(viewsRef.current.keys()));\n    return () => {\n      viewsRef.current.delete(key);\n      setOrderedKeys((prev) => prev.filter((k) => k !== key));\n    };\n  }, []);\n\n  // Track the previous active key for direction calc. Render-time conditional\n  // setters below are safe: React discards and retries the render, so values\n  // read after the `if` reflect the committed state.\n  const [previousKey, setPreviousKey] = React.useState(activeKey);\n  const [renderedKey, setRenderedKey] = React.useState(activeKey);\n  if (activeKey !== renderedKey) {\n    setPreviousKey(renderedKey);\n    setRenderedKey(activeKey);\n  }\n\n  // Height animation, swaps only. The observer (useAnimatedHeight) tracks\n  // content size with INSTANT writes — animating those would fight a tween the\n  // content is already running — so the swap's height change animates here\n  // instead: a one-shot WAAPI animation started at swap commit. Old and new\n  // heights are measured synchronously in the layout effect, so a same-height\n  // swap (or reduced motion) simply never starts one — no transition classes,\n  // no `transitionend` bookkeeping, no fallback timers. The observer's instant\n  // style write lands underneath the running animation (WAAPI overrides it)\n  // and is already correct when the animation finishes; if the incoming view\n  // resizes mid-flight, `handleContentResize` retargets so it stays that way.\n  // An interrupted swap cancels the old animation and retargets from the\n  // mid-flight height (offsetHeight reflects the animated value).\n  const hasSwappedRef = React.useRef(false);\n  React.useLayoutEffect(() => {\n    // First run is mount, not a swap — nothing to animate.\n    if (!hasSwappedRef.current) {\n      hasSwappedRef.current = true;\n      return;\n    }\n    const outer = outerRef.current;\n    const inner = innerDivRef.current;\n    if (!outer || !inner) return;\n    animateHeight(outer, inner.offsetHeight, heightState.current);\n    // `outerRef` is a stable RefObject; listed to satisfy exhaustive-deps.\n  }, [renderedKey, outerRef]);\n\n  // Direction from registry order (not React.Children, so views can be wrapped\n  // / conditional / Suspense-gated). On first render `orderedKeys` is empty and\n  // direction defaults to forward, but `mounted` is also false then so no slide\n  // observes the placeholder.\n  const currentIdx = orderedKeys.indexOf(activeKey);\n  const previousIdx = orderedKeys.indexOf(previousKey);\n  const direction = currentIdx >= previousIdx ? 1 : -1;\n\n  // False only on initial render (both keys start equal); the first swap flips\n  // it permanently. Gates `data-activation-direction` so consumers can tell\n  // \"first paint\" from a real swap.\n  const hasActivated = activeKey !== previousKey;\n\n  // Dev warning, gated on a populated registry (view layout effects run after\n  // this render's body on the initial mount).\n  if (process.env.NODE_ENV !== \"production\") {\n    if (orderedKeys.length > 0 && !orderedKeys.includes(activeKey)) {\n      console.warn(\n        `[TransitionPanel] activeKey=\"${activeKey}\" doesn't match any ` +\n          `registered TransitionPanelView viewKey. ` +\n          `Registered: ${orderedKeys.join(\", \")}.`,\n      );\n    }\n  }\n\n  // Skip `@starting-style` on first paint so the initial view doesn't animate\n  // in on mount. The `requestAnimationFrame` defer is required: without it,\n  // mobile Chrome still triggers `@starting-style` and slides the content in.\n  const [mounted, setMounted] = React.useState(false);\n  React.useEffect(() => {\n    const id = requestAnimationFrame(() => setMounted(true));\n    return () => cancelAnimationFrame(id);\n  }, []);\n\n  // Focus management for swaps. Views stay mounted, so \"autoFocus on mount\"\n  // can't fire on re-activation. `useLayoutEffect` (not `useEffect`) runs the\n  // focus move before paint, keeping the inert-flip-to-refocus gap within a\n  // frame. The one-shot mount flag skips first render (the parent owns initial\n  // focus); a key-comparison gate would instead break back-navigation focus.\n  const hasMountedRef = React.useRef(false);\n  React.useLayoutEffect(() => {\n    if (!hasMountedRef.current) {\n      hasMountedRef.current = true;\n      return;\n    }\n    const entry = viewsRef.current.get(activeKey);\n    if (!entry || entry.initialFocus === false) return;\n    const target =\n      entry.initialFocus === true\n        ? entry.el.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)\n        : entry.initialFocus.current;\n    // `preventScroll` avoids scroll-jank from the browser chasing the focused\n    // element while the entrance animation is still translating it in.\n    target?.focus({ preventScroll: true });\n  }, [activeKey]);\n\n  // Direction-aware slide offsets, passed to views via context as CSS vars\n  // (the class names are literal in the view source; only the values change).\n  // Consumed in `slide` mode only.\n  const enterFrom = direction === 1 ? SLIDE_DISTANCE : `-${SLIDE_DISTANCE}`;\n  const exitTo = direction === 1 ? `-${SLIDE_DISTANCE}` : SLIDE_DISTANCE;\n\n  const contextValue = React.useMemo<TransitionPanelContextValue>(\n    () => ({\n      activeKey,\n      transition,\n      enterFrom,\n      exitTo,\n      mounted,\n      registerView,\n    }),\n    [activeKey, transition, enterFrom, exitTo, mounted, registerView],\n  );\n\n  // `data-activation-direction` matches Base UI's Tabs.Panel vocabulary:\n  // \"left\" | \"right\" | \"none\" (slide is horizontal-only; \"none\" pre-activation).\n  // Reported in `fade` mode too, for consumers driving their own directional\n  // styling even though the built-in fade is non-directional.\n  const activationDirection: \"left\" | \"right\" | \"none\" = !hasActivated\n    ? \"none\"\n    : direction === 1\n      ? \"right\"\n      : \"left\";\n\n  const defaultProps = {\n    \"data-slot\": \"transition-panel\",\n    \"data-transition\": transition,\n    \"data-activation-direction\": activationDirection,\n    // Defaults for the inherited CSS vars. Consumer `style` spreads after, so\n    // any override wins. `--tp-fade-duration` falls back to the observer-written\n    // `--fade-duration` (adaptive crossfade) until overridden to a fixed value;\n    // `--tp-clip-margin` is the overflow-clip bleed (0 by default; bump it when\n    // view content sits flush with a padded container's edge).\n    style: {\n      \"--tp-duration\": \"240ms\",\n      \"--tp-fade-duration\": \"var(--fade-duration, var(--tp-duration))\",\n      \"--tp-ease\": \"cubic-bezier(0.32, 0.72, 0, 1)\",\n      \"--tp-fade-ease\": \"cubic-bezier(0.26, 0.08, 0.25, 1)\",\n      \"--tp-clip-margin\": \"0px\",\n      ...style,\n    } as React.CSSProperties,\n    className: cn(\n      // `overflow-clip` (not `-hidden`): hidden still permits programmatic\n      // `scrollIntoView` (e.g. focusing a deep descendant), which would scroll\n      // this container and expose the clipped region; clip is a true non-scroll\n      // container. `overflow-clip-margin` adds a bleed for focus rings / shadows\n      // (see `--tp-clip-margin`). `contain-layout` scopes the height-driven\n      // layout work here. Side effect: it becomes a containing block for\n      // fixed/absolute descendants (flag if you position one inside).\n      \"overflow-clip contain-layout [overflow-clip-margin:var(--tp-clip-margin)]\",\n      // Swap height changes animate via the WAAPI one-shot above, not a\n      // transition class — so untouched height writes stay instant.\n      \"motion-reduce:transition-none\",\n      className,\n    ),\n    children: (\n      <div ref={setInnerRef} className=\"grid grid-cols-[minmax(0,1fr)]\">\n        <TransitionPanelContext value={contextValue}>\n          {children}\n        </TransitionPanelContext>\n      </div>\n    ),\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    render,\n    ref: outerRef,\n    props: mergeProps<\"div\">(defaultProps, props),\n  });\n}\n\nTransitionPanel.displayName = \"TransitionPanel\";\n\ntype HeightAnimationState = {\n  animation: Animation | null;\n  /** Height the running animation is heading toward, for staleness checks. */\n  target: number | null;\n};\n\n/**\n * Start (or restart) the root's one-shot height animation, reading duration and\n * easing from `--tp-duration` / `--tp-ease` at call time so overrides apply.\n * No-ops when the height isn't actually changing or under reduced motion, which\n * is what keeps a same-height swap from needing any cleanup. `from` is read\n * before cancelling any in-flight animation, so an interrupted or retargeted\n * swap continues from the mid-flight height rather than snapping back.\n */\nfunction animateHeight(\n  outer: HTMLElement,\n  to: number,\n  state: HeightAnimationState,\n): void {\n  const from = outer.offsetHeight;\n  if (from === to) return;\n  if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n  const styles = getComputedStyle(outer);\n  const duration =\n    parseCssTime(styles.getPropertyValue(\"--tp-duration\")) ?? 240;\n  const easing =\n    styles.getPropertyValue(\"--tp-ease\").trim() ||\n    \"cubic-bezier(0.32, 0.72, 0, 1)\";\n  state.animation?.cancel();\n  state.target = to;\n  state.animation = outer.animate(\n    { height: [`${from}px`, `${to}px`] },\n    { duration, easing },\n  );\n}\n\n/**\n * Parse a CSS time (\"240ms\" / \"0.24s\") to milliseconds. A unitless value is\n * invalid CSS for a time, but if a consumer writes one anyway, reading it as\n * milliseconds (240 → 240ms) fails far less spectacularly than as seconds\n * (240 → 4 minutes).\n */\nfunction parseCssTime(value: string): number | null {\n  const trimmed = value.trim();\n  if (!trimmed) return null;\n  const parsed = parseFloat(trimmed);\n  if (!Number.isFinite(parsed)) return null;\n  if (trimmed.endsWith(\"ms\")) return parsed;\n  if (trimmed.endsWith(\"s\")) return parsed * 1000;\n  return parsed;\n}\n\nfunction TransitionPanelView({\n  viewKey,\n  initialFocus = true,\n  render,\n  className,\n  style,\n  children,\n  ...props\n}: TransitionPanelViewProps) {\n  const ctx = React.use(TransitionPanelContext);\n  if (!ctx) {\n    throw new Error(\n      \"TransitionPanelView must be rendered inside a TransitionPanel.\",\n    );\n  }\n  const { activeKey, transition, enterFrom, exitTo, mounted, registerView } =\n    ctx;\n  const isActive = viewKey === activeKey;\n  const isFade = transition === \"fade\";\n  const cssExit = useCssExitSupported();\n  const wrapperRef = React.useRef<HTMLDivElement | null>(null);\n\n  // useLayoutEffect (not useEffect), plus child-to-parent ordering, makes a\n  // view register before the panel's focus move reads the registry, even\n  // when it mounts in the same render as the activeKey change.\n  React.useLayoutEffect(() => {\n    const el = wrapperRef.current;\n    if (!el) return;\n    return registerView(viewKey, el, initialFocus);\n  }, [viewKey, initialFocus, registerView]);\n\n  // JS-path exit hide (Firefox only; no-ops on the CSS path, see\n  // `supportsDiscreteDisplayExit`). Lets the leaving view animate out as\n  // `block`, then sets `display:none` once its transitions finish. We wait on\n  // the Web Animations API (`el.getAnimations()`, then each `.finished`), like\n  // Base UI's `useAnimationsFinished`: it covers every animated property at\n  // once, is exact (no duration math), resolves right away under reduced motion,\n  // and re-checks on abort so an interrupted swap waits for whatever replaced it.\n  const [hidden, setHidden] = React.useState(!isActive);\n  React.useEffect(() => {\n    if (cssExit) return;\n    if (isActive) {\n      setHidden(false);\n      return;\n    }\n    if (hidden) return;\n    const el = wrapperRef.current;\n    if (!el || typeof el.getAnimations !== \"function\") {\n      setHidden(true);\n      return;\n    }\n    let cancelled = false;\n    const waitForExit = () => {\n      Promise.all(el.getAnimations().map((animation) => animation.finished))\n        .then(() => {\n          if (!cancelled) setHidden(true);\n        })\n        .catch(() => {\n          // A transition was aborted (e.g. an interrupted swap replaced it).\n          // Wait for whatever is running now; if nothing is, just hide.\n          if (cancelled) return;\n          if (el.getAnimations().length > 0) waitForExit();\n          else setHidden(true);\n        });\n    };\n    // Defer a frame so the exit transition is registered before we read it.\n    const id = requestAnimationFrame(waitForExit);\n    return () => {\n      cancelled = true;\n      cancelAnimationFrame(id);\n    };\n  }, [cssExit, isActive, hidden]);\n\n  const defaultProps = {\n    \"aria-hidden\": !isActive,\n    inert: !isActive,\n    \"data-slot\": \"transition-panel-view\",\n    \"data-active\": isActive ? \"\" : undefined,\n    \"data-viewkey\": viewKey,\n    style: {\n      // Consumer style spreads first; the panel-controlled slide vars override\n      // after (they change every render, so a consumer value would just break\n      // the slide). Unused in `fade` mode.\n      ...style,\n      \"--tp-enter\": enterFrom,\n      \"--tp-exit\": exitTo,\n    } as React.CSSProperties,\n    className: cn(\n      \"[grid-area:1/1]\",\n      // Transitioned properties. `slide` translates on --tp-duration/--tp-ease;\n      // `fade` scales + crossfades on --tp-fade-duration/--tp-fade-ease. `display`\n      // is included only on the CSS-exit path (the JS path hides via the effect\n      // above).\n      isFade\n        ? cssExit\n          ? \"transition-[opacity,scale,display]\"\n          : \"transition-[opacity,scale]\"\n        : cssExit\n          ? \"transition-[opacity,translate,display]\"\n          : \"transition-[opacity,translate]\",\n      isFade\n        ? \"duration-(--tp-fade-duration) ease-(--tp-fade-ease)\"\n        : \"duration-(--tp-duration) ease-(--tp-ease)\",\n      cssExit && \"transition-discrete\",\n      \"motion-reduce:transition-none\",\n      mounted && \"starting:opacity-0\",\n      // Slide sets the `translate` property *directly* rather than via Tailwind's\n      // `translate-x-*` (which routes through the `@property`-registered var\n      // `--tw-translate-x`). WebKit drops an `@starting-style` value on a\n      // registered custom property when it's a `var()` reference, falling back to\n      // the registered `initial-value: 0`. So `--tw-translate-x: var(--tp-enter)`\n      // collapses to 0 and the entering view jumps in with no slide (only the\n      // crossfade survives). Chrome resolves it fine. Setting `translate` directly\n      // bypasses the registered var. Fade's `scale-*` is a literal, so it's safe.\n      mounted &&\n        (isFade\n          ? \"starting:scale-[0.96]\"\n          : \"starting:[translate:var(--tp-enter)_0]\"),\n      isActive\n        ? isFade\n          ? \"scale-100 opacity-100\"\n          : \"[translate:0_0] opacity-100\"\n        : cn(\n            \"pointer-events-none opacity-0 contain-[size]\",\n            isFade ? \"scale-[0.96]\" : \"[translate:var(--tp-exit)_0]\",\n            // CSS path hides now (`allow-discrete` defers the real\n            // `display:none`); JS path waits for the deferred-hide effect above.\n            (cssExit || hidden) && \"hidden\",\n          ),\n      className,\n    ),\n    children,\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    render,\n    ref: wrapperRef,\n    props: mergeProps<\"div\">(defaultProps, props),\n  });\n}\n\nTransitionPanelView.displayName = \"TransitionPanelView\";\n\nexport { TransitionPanel, TransitionPanelView };\nexport type {\n  TransitionPanelProps,\n  TransitionPanelViewProps,\n  TransitionPanelInitialFocus,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/transition-panel.tsx"
    }
  ],
  "type": "registry:ui"
}