{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "exploded-ui",
  "type": "registry:ui",
  "title": "Exploded UI",
  "description": "A product screenshot that comes apart into its layers as you scroll, explains each one, then clicks back together. The layers are real HTML, so text stays sharp and follows your theme.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/exploded-ui.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  animate,\n  cancelFrame,\n  cubicBezier,\n  frame,\n  motionValue,\n  useScroll,\n  useSpring,\n  type MotionValue,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/* ─────────────────────────────────────────────────────────\n * EXPLODED UI STORYBOARD   (driven by scroll progress, not time)\n *\n *     0%   the section sticks with the screenshot centered.\n *          It's plain, flat HTML: no 3D, pin-sharp\n *  15→35%  the plane tilts back and turns, still one piece,\n *          and shrinks to make room for the stack. A faint\n *          grid floor fades in under it\n *  32→55%  the layers lift off top first, each gap opening\n *          before the one beneath it has finished, so the\n *          stack peels apart in one motion. Each layer casts\n *          a shadow that softens as its gap widens. Leaders\n *          draw out from the layers, then the labels fade in\n *  56→85%  walkthrough: one layer at a time lifts a little\n *          further while the rest fade back, and its label\n *          opens to show its description. Layers above it\n *          fade out to their outlines, so it's never covered\n *  85→97%  the gaps close bottom first, the plane tilts flat\n *          and lands exactly where it started\n *    100%  the section unsticks and the page carries on\n *\n *  Scrolling up plays it backwards. end=\"exploded\" gives the\n *  walkthrough the rest of the scroll and leaves the stack\n *  open. While it's open, hovering a layer or its label does\n *  what the walkthrough does, and the pointer turns the stack\n *  a few degrees.\n *\n *  Every frame is one pose: the plane's angle, scale and\n *  offset, and each layer's height. The layers are drawn\n *  from it in 3D, and the labels from the same pose projected\n *  flat, so they stay on their layers and never skew or blur.\n * ───────────────────────────────────────────────────────── */\n\nconst EXPLODE = {\n  back: 50, // degrees the plane tilts away, by default\n  turn: -30, // degrees it turns on the page before it tilts, by default\n  spread: 0.12, // gap between layers, as a share of the plane's width\n  scale: 0.85, // the largest the tilted plane gets, as a share of its size\n  overlap: 0.55, // share of each gap's opening that overlaps the next one's\n  lift: 0.35, // how much higher the active layer rises, in gaps\n  // How much the other layers fade while one is active. Those above it are\n  // in the way, so they fade out entirely and leave their outline.\n  fade: { below: 0.65, above: 1 },\n  pointer: 4, // degrees the pointer turns the open stack, either way\n  perspective: 3, // perspective distance, in plane widths\n  floor: 0.6, // how far the grid floor sits under the base, in gaps\n  narrow: 640, // px: narrower stages get numbered labels and a gentler tilt\n  gentle: 0.8, // share of the tilt kept on narrow stages\n  ease: cubicBezier(0.65, 0, 0.35, 1), // each beat's pace\n};\n\n// Where each beat sits, as shares of the time the section is stuck.\nconst BEATS = {\n  tilt: [0.15, 0.35],\n  explode: [0.32, 0.55],\n  tour: [0.56, 0.85],\n  collapse: [0.85, 0.93],\n  flatten: [0.9, 0.97],\n} as const;\n\n// With end=\"exploded\", the walkthrough takes the rest of the scroll.\nconst OPEN_TOUR = [0.56, 0.95] as const;\n\nconst SCROLL_SPRING = { stiffness: 120, damping: 30, mass: 0.4 };\nconst POINTER_SPRING = { stiffness: 70, damping: 18, mass: 1 };\nconst LIFT_SPRING = { type: \"spring\", visualDuration: 0.45, bounce: 0.2 } as const;\nconst DIM_SPRING = { type: \"spring\", visualDuration: 0.35, bounce: 0 } as const;\n\nconst LABEL = {\n  leader: 40, // px from the furthest anchor to the label column\n  knee: 16, // px before the column where a leader bends toward its label\n  gap: 10, // px kept between labels\n  dim: 0.6, // how much the other labels fade while one is active\n  pad: 20, // px the stack and labels keep from the stage's edges\n};\n\n// A layer fills the plane unless its classes place or size it.\nconst PLACED = /(?:^|\\s|:)-?(?:inset|top|right|bottom|left|start|end|w|h|size)-/;\n\nexport interface ExplodedUILayerProps {\n  /** Shown beside the layer once the stack is open. Layers without one aren't labeled or visited by the walkthrough. */\n  label?: React.ReactNode;\n  /** One line about the layer, shown while it's active. */\n  description?: React.ReactNode;\n  /** Places and sizes the layer on the plane, like `left-0 top-0 h-full w-[22%]`. Without any, it fills the plane. */\n  className?: string;\n  /** Drops the sheet: no clipping, edges or shadow. For floating pieces like a cursor. */\n  bare?: boolean;\n  children?: React.ReactNode;\n}\n\n/** One layer of the stack. Its place among its siblings sets its depth: the first is the bottom. */\nexport function ExplodedUILayer(props: ExplodedUILayerProps): React.ReactElement | null {\n  void props; // ExplodedUI reads the props and draws the layer itself.\n  return null;\n}\n\nexport interface ExplodedUIProps extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n  /** The layers, as `ExplodedUI.Layer`s. The first child is the bottom of the stack. */\n  children: React.ReactNode;\n  /** Step through the labeled layers one at a time once the stack is open. */\n  walkthrough?: boolean;\n  /** The viewing angle once tilted, in degrees. `back` tilts the plane away; `turn` turns it on the page first. */\n  tilt?: { back?: number; turn?: number };\n  /** Gap between layers once open, as a share of the plane's width. */\n  spread?: number;\n  /** How long the section stays stuck, in stage heights. Defaults to 3, or 4 with a walkthrough. */\n  scrollLength?: number;\n  /** Land back as the flat screenshot, or stay open as the section scrolls away. */\n  end?: \"assembled\" | \"exploded\";\n  /** Labels beside the stack, numbered markers with a list below it, or none. Side labels become numbered on narrow stages. */\n  labels?: \"side\" | \"numbered\" | false;\n  /** Hover or focus to highlight a layer, and let the pointer turn the open stack. */\n  interactive?: boolean;\n  /** Called with a layer's index as the walkthrough reaches it, and with null as it leaves the layers. */\n  onLayerChange?: (index: number | null) => void;\n  /**\n   * Scroll container to track. Defaults to the window. The container must be\n   * positioned (e.g. `relative`), or scroll progress is measured from the page.\n   */\n  containerRef?: React.RefObject<HTMLElement | null>;\n  /** Classes for the stuck stage. Set its height and background here. */\n  stageClassName?: string;\n  /** Classes for the plane: the screenshot's box. Size it here. */\n  className?: string;\n}\n\ntype Box = { x: number; y: number; w: number; h: number };\ntype Point = { x: number; y: number };\ntype Pose = {\n  back: number;\n  turn: number;\n  /** The pointer's turn, in degrees. */\n  px: number;\n  py: number;\n  scale: number;\n  /** Screen offset, in px. */\n  tx: number;\n  ty: number;\n  /** How far the stack sinks so it stays centered on its middle, in px. */\n  sink: number;\n};\n\n/** Everything measured: redone on resize, read every frame. */\ntype Geometry = {\n  stage: { w: number; h: number };\n  plane: Box;\n  boxes: Box[];\n  gap: number;\n  depth: number;\n  back: number;\n  turn: number;\n  /** The tilted plane's scale and offset, so the open stack and its labels fit the stage. */\n  fit: { scale: number; tx: number; ty: number };\n  /** Where each label's leader or marker attaches, on its layer. */\n  anchors: Point[];\n  /** Labeled layers, top to bottom on screen. */\n  order: number[];\n  /** Where the numbered list starts, under the open stack. */\n  listTop: number;\n  labels: { title: number; desc: number }[];\n  /** The numbered markers' size, in px. */\n  marker: number;\n  shadows: { on: number; from: number; contact: HTMLElement; soft: HTMLElement }[];\n};\n\n/** What the pointer needs from the last frame. */\ntype Live = { pose: Pose; heights: number[]; open: boolean };\n\nfunction ExplodedUIRoot({\n  children,\n  walkthrough = false,\n  tilt,\n  spread = EXPLODE.spread,\n  scrollLength = walkthrough ? 4 : 3,\n  end = \"assembled\",\n  labels = \"side\",\n  interactive = true,\n  onLayerChange,\n  containerRef,\n  stageClassName,\n  className,\n  ...props\n}: ExplodedUIProps) {\n  const still = usePrefersReducedMotion();\n  const layers = React.Children.toArray(children).filter(\n    (child): child is React.ReactElement<ExplodedUILayerProps> => React.isValidElement(child),\n  );\n  const count = layers.length;\n  const labeled = layers.flatMap((layer, i) => (layer.props.label ? [i] : []));\n  const labeledKey = labeled.join(\",\");\n  const bareKey = layers.map((layer) => (layer.props.bare ? 1 : 0)).join(\"\");\n  const back = tilt?.back ?? EXPLODE.back;\n  const turn = tilt?.turn ?? EXPLODE.turn;\n\n  const sectionRef = React.useRef<HTMLElement>(null);\n  const stageRef = React.useRef<HTMLDivElement>(null);\n  const planeRef = React.useRef<HTMLDivElement>(null);\n  const rigRef = React.useRef<HTMLDivElement>(null);\n  const floorRef = React.useRef<HTMLDivElement>(null);\n  const floorShadowRef = React.useRef<HTMLDivElement>(null);\n  const listRef = React.useRef<HTMLDivElement>(null);\n  const layerEls = React.useRef<(HTMLDivElement | null)[]>([]);\n  const edgeEls = React.useRef<(HTMLSpanElement | null)[]>([]);\n  const outlineEls = React.useRef<(HTMLSpanElement | null)[]>([]);\n  const shadowEls = React.useRef(new Map<string, HTMLSpanElement>());\n  const labelEls = React.useRef<(HTMLElement | null)[]>([]);\n  const titleEls = React.useRef<(HTMLElement | null)[]>([]);\n  const descEls = React.useRef<(HTMLElement | null)[]>([]);\n  const lineEls = React.useRef<(SVGPolylineElement | null)[]>([]);\n  const dotEls = React.useRef<(SVGCircleElement | null)[]>([]);\n  const markerEls = React.useRef<(HTMLElement | null)[]>([]);\n  const geoRef = React.useRef<Geometry | null>(null);\n  const liveRef = React.useRef<Live | null>(null);\n  const onLayerChangeRef = React.useRef(onLayerChange);\n  const tourRef = React.useRef<number | null>(null);\n\n  const [narrow, setNarrow] = React.useState(false);\n  const [stageHeight, setStageHeight] = React.useState(0);\n  const [tour, setTour] = React.useState<number | null>(null);\n  const [hovered, setHovered] = React.useState<number | null>(null);\n  const [pointed, setPointed] = React.useState<number | null>(null);\n  const [pinned, setPinned] = React.useState<number | null>(null);\n  const active = interactive ? (pointed ?? hovered ?? pinned ?? tour) : tour;\n  const mode = labels === \"side\" && narrow ? \"numbered\" : labels;\n\n  const { scrollYProgress } = useScroll({\n    target: sectionRef,\n    container: containerRef,\n    offset: [\"start start\", \"end end\"],\n  });\n  const progress = useSpring(scrollYProgress, SCROLL_SPRING);\n  const turnX = useSpring(0, POINTER_SPRING);\n  const turnY = useSpring(0, POINTER_SPRING);\n  // Per layer: how far it has risen as the active one, how much it has faded,\n  // and how far it has turned into its outline above the active one.\n  const focus = React.useMemo(\n    () =>\n      Array.from({ length: count }, () => ({\n        lift: motionValue(0),\n        dim: motionValue(0),\n        ghost: motionValue(0),\n      })),\n    [count],\n  );\n\n  React.useEffect(() => {\n    onLayerChangeRef.current = onLayerChange;\n  });\n\n  React.useEffect(() => {\n    focus.forEach(({ lift, dim, ghost }, i) => {\n      const above = active !== null && i > active;\n      animate(lift, !still && active === i ? 1 : 0, LIFT_SPRING);\n      animate(dim, active === null || active === i ? 0 : above ? EXPLODE.fade.above : EXPLODE.fade.below, DIM_SPRING);\n      animate(ghost, above ? 1 : 0, DIM_SPRING);\n    });\n  }, [active, focus, still]);\n\n  React.useLayoutEffect(() => {\n    const stage = stageRef.current;\n    const plane = planeRef.current;\n    const rig = rigRef.current;\n    if (!stage || !plane || !rig || !count) return;\n\n    const marked = labeledKey ? labeledKey.split(\",\").map(Number) : [];\n    const tourRange = end === \"assembled\" ? BEATS.tour : OPEN_TOUR;\n    const gaps = new Array<number>(count).fill(0);\n    const heights = new Array<number>(count).fill(0);\n    let wasOpen = false;\n\n    const measure = () => {\n      const stageBox = stage.getBoundingClientRect();\n      const planeBox = plane.getBoundingClientRect();\n      const w = plane.offsetWidth;\n      const h = plane.offsetHeight;\n      if (!w || !h) return;\n\n      const isNarrow = stageBox.width < EXPLODE.narrow;\n      setNarrow(isNarrow);\n      setStageHeight(stage.offsetHeight);\n      // The other kind of labels renders first, then this runs again.\n      if ((labels === \"side\" && isNarrow ? \"numbered\" : labels) !== mode) return;\n\n      const gentle = isNarrow ? EXPLODE.gentle : 1;\n      const boxes = Array.from({ length: count }, (_, i) => {\n        const el = layerEls.current[i];\n        return el\n          ? { x: el.offsetLeft, y: el.offsetTop, w: el.offsetWidth, h: el.offsetHeight }\n          : { x: 0, y: 0, w, h };\n      });\n      const sizes = Array.from({ length: count }, (_, i) => ({\n        title: titleEls.current[i]?.offsetHeight ?? 0,\n        desc: descEls.current[i]?.offsetHeight ?? 0,\n        w: Math.max(titleEls.current[i]?.offsetWidth ?? 0, descEls.current[i]?.offsetWidth ?? 0),\n      }));\n      const widest = Math.max(0, ...marked.map((i) => sizes[i].w));\n\n      const geo: Geometry = {\n        stage: { w: stage.offsetWidth, h: stage.offsetHeight },\n        plane: { x: planeBox.left - stageBox.left, y: planeBox.top - stageBox.top, w, h },\n        boxes,\n        gap: spread * w,\n        depth: EXPLODE.perspective * Math.max(w, h),\n        back: back * gentle,\n        turn: turn * gentle,\n        fit: { scale: 1, tx: 0, ty: 0 },\n        anchors: [],\n        order: [],\n        listTop: 0,\n        labels: sizes,\n        marker: markerEls.current[marked[0]]?.offsetWidth ?? 0,\n        shadows: [],\n      };\n      fitStack(geo, {\n        kind: mode === \"numbered\" ? \"numbered\" : \"side\",\n        side: mode === \"side\" ? LABEL.leader + widest : 0,\n        below: mode === \"numbered\" ? (listRef.current?.offsetHeight ?? 0) : 0,\n        marked,\n      });\n      if (listRef.current) listRef.current.style.top = `${geo.listTop}px`;\n\n      // Each layer's shadow falls on every sheet under it, clipped to that sheet.\n      for (const [key, el] of shadowEls.current) {\n        const [on, from, kind] = key.split(\":\");\n        const a = boxes[Number(on)];\n        const b = boxes[Number(from)];\n        const overlaps = b.x < a.x + a.w && a.x < b.x + b.w && b.y < a.y + a.h && a.y < b.y + b.h;\n        el.style.display = overlaps ? \"\" : \"none\";\n        if (!overlaps) continue;\n        el.style.left = `${b.x - a.x}px`;\n        el.style.top = `${b.y - a.y}px`;\n        el.style.width = `${b.w}px`;\n        el.style.height = `${b.h}px`;\n        el.style.filter = `blur(${(kind === \"contact\" ? 0.004 : 0.02) * w}px)`;\n        if (kind === \"soft\") {\n          const contact = shadowEls.current.get(`${on}:${from}:contact`);\n          if (contact) geo.shadows.push({ on: Number(on), from: Number(from), contact, soft: el });\n        }\n      }\n      if (floorShadowRef.current) floorShadowRef.current.style.filter = `blur(${0.04 * w}px)`;\n\n      // Outlines sit beside their layers, not in them, so they stay when the\n      // layer and anything styled on it fade out.\n      boxes.forEach((box, i) => {\n        const outline = outlineEls.current[i];\n        const layer = layerEls.current[i];\n        if (!outline || !layer) return;\n        outline.style.left = `${box.x}px`;\n        outline.style.top = `${box.y}px`;\n        outline.style.width = `${box.w}px`;\n        outline.style.height = `${box.h}px`;\n        outline.style.borderRadius = getComputedStyle(layer).borderRadius;\n      });\n\n      geoRef.current = geo;\n      schedule();\n    };\n\n    const update = () => {\n      const g = geoRef.current;\n      if (!g) return;\n      const p = progress.get();\n\n      // How tilted the plane is, and how open each gap is.\n      let tilted = 1;\n      if (still) {\n        gaps.fill(1);\n        gaps[0] = 0;\n      } else {\n        tilted = EXPLODE.ease(segment(p, BEATS.tilt));\n        if (end === \"assembled\") tilted *= 1 - EXPLODE.ease(segment(p, BEATS.flatten));\n        openGaps(p, end, gaps);\n      }\n      let rise = 0;\n      for (let i = 0; i < count; i++) {\n        rise += gaps[i] * g.gap;\n        heights[i] = rise;\n      }\n      // Open is the last gap to open and the first to close.\n      const open = count > 1 ? gaps[1] : tilted;\n      const pose: Pose = {\n        back: g.back * tilted,\n        turn: g.turn * tilted,\n        px: turnX.get() * open,\n        py: turnY.get() * open,\n        scale: 1 + (g.fit.scale - 1) * tilted,\n        tx: g.fit.tx * tilted,\n        ty: g.fit.ty * tilted,\n        sink: heights[count - 1] / 2,\n      };\n      const isOpen = still || (tilted > 0.98 && open > 0.98);\n      liveRef.current = { pose, heights: heights.slice(), open: isOpen };\n\n      // The walkthrough's layer.\n      let nextTour: number | null = null;\n      if (walkthrough && !still && marked.length) {\n        const u = (p - tourRange[0]) / (tourRange[1] - tourRange[0]);\n        if (u >= 0 && u < 1) nextTour = marked[Math.floor(u * marked.length)];\n      }\n      if (nextTour !== tourRef.current) {\n        tourRef.current = nextTour;\n        setTour(nextTour);\n        // Scrolling takes over from the pointer.\n        setHovered(null);\n        setPinned(null);\n        onLayerChangeRef.current?.(nextTour);\n      }\n      // Closing the stack lets go of anything the pointer picked.\n      if (wasOpen && !isOpen) {\n        setHovered(null);\n        setPinned(null);\n        setPointed(null);\n      }\n      wasOpen = isOpen;\n\n      // Flat and still, nothing is 3D, so the text renders pin-sharp.\n      const z = heights.map((rest, i) => rest + focus[i].lift.get() * EXPLODE.lift * g.gap);\n      const flat = tilted === 0 && z.every((v) => v === 0);\n      rig.style.transform = flat ? \"none\" : rigTransform(pose);\n      plane.style.perspective = flat ? \"\" : `${g.depth}px`;\n\n      for (let i = 0; i < count; i++) {\n        const el = layerEls.current[i];\n        if (!el) continue;\n        el.style.transform = flat ? \"none\" : `translateZ(${z[i]}px)`;\n        const fade = 1 - focus[i].dim.get();\n        el.style.opacity = fade < 1 ? String(fade) : \"\";\n        const outline = outlineEls.current[i];\n        if (outline) {\n          outline.style.transform = flat ? \"none\" : `translateZ(${z[i]}px)`;\n          outline.style.opacity = String(focus[i].ghost.get());\n        }\n        const edge = edgeEls.current[i];\n        if (edge) edge.style.opacity = String(i === 0 ? tilted : Math.min(1, gaps[i] * 2));\n      }\n\n      // A shadow starts crisp and dark, then spreads and fades as its gap\n      // widens. A layer fading back casts a fainter one.\n      for (const { on, from, contact, soft } of g.shadows) {\n        const lift = (z[from] - z[on]) / g.gap;\n        const cast = 1 - focus[from].dim.get();\n        contact.style.opacity = String(cast * 0.22 * smooth(0, 0.15, lift) * (1 - smooth(0.2, 1, lift)));\n        soft.style.opacity = String((cast * 0.13 * smooth(0.05, 0.8, lift)) / (1 + 0.5 * Math.max(0, lift - 1)));\n        soft.style.transform = `translate(${0.04 * lift * g.gap}px, ${0.16 * lift * g.gap}px) scale(${1 + 0.02 * lift})`;\n      }\n\n      const floor = floorRef.current;\n      if (floor) {\n        floor.style.opacity = String(tilted);\n        floor.style.visibility = tilted > 0 ? \"visible\" : \"hidden\";\n        floor.style.transform = `translateZ(${-EXPLODE.floor * g.gap}px)`;\n      }\n      if (floorShadowRef.current) {\n        floorShadowRef.current.style.opacity = String(open * (1 - focus[0].dim.get()));\n      }\n\n      if (mode) drawLabels(g, pose, z, gaps, tilted);\n    };\n\n    const drawLabels = (g: Geometry, pose: Pose, z: number[], gaps: number[], tilted: number) => {\n      const project = projector(pose, g);\n      const at = (i: number) => {\n        const a = project(g.anchors[i].x, g.anchors[i].y, z[i]);\n        return { x: a.x + g.plane.x, y: a.y + g.plane.y };\n      };\n      // A label shows up as its layer lifts off; the base's, as the last gap opens.\n      const reveal = (i: number) =>\n        still ? 1 : count > 1 ? smooth(i === 0 ? 0.6 : 0.3, 1, gaps[Math.max(1, i)]) : tilted;\n\n      if (mode === \"numbered\") {\n        for (const i of g.order) {\n          const marker = markerEls.current[i];\n          if (!marker) continue;\n          const a = at(i);\n          const r = reveal(i);\n          const size = g.marker;\n          marker.style.visibility = r > 0 ? \"visible\" : \"hidden\";\n          marker.style.opacity = String(r * (1 - LABEL.dim * dimmed(focus[i].dim.get())));\n          marker.style.transform = `translate3d(${Math.round(a.x - size / 2)}px, ${Math.round(a.y - size / 2)}px, 0) scale(${0.6 + 0.4 * r + 0.15 * clamp01(focus[i].lift.get())})`;\n          const chip = labelEls.current[i];\n          if (chip) {\n            chip.style.visibility = r > 0 ? \"visible\" : \"hidden\";\n            chip.style.opacity = String(r);\n          }\n        }\n        return;\n      }\n\n      const anchors = g.order.map(at);\n      const column = Math.max(...anchors.map((a) => a.x)) + LABEL.leader;\n      const items = g.order.map((i, k) => {\n        const open = still ? 1 : clamp01(focus[i].lift.get());\n        return {\n          i,\n          anchor: anchors[k],\n          open,\n          top: anchors[k].y - g.labels[i].title / 2,\n          height: g.labels[i].title + (g.labels[i].desc + 4) * open,\n          weight: 1 + 4 * open,\n        };\n      });\n      stack(items, LABEL.pad, g.stage.h - LABEL.pad);\n\n      for (const { i, anchor, open, top } of items) {\n        const label = labelEls.current[i];\n        const line = lineEls.current[i];\n        const dot = dotEls.current[i];\n        const r = reveal(i);\n        const drawn = clamp01(r / 0.6);\n        const shown = smooth(0.45, 1, r);\n        const fade = 1 - LABEL.dim * dimmed(focus[i].dim.get());\n        const y = top + g.labels[i].title / 2;\n        if (label) {\n          label.style.visibility = shown > 0 ? \"visible\" : \"hidden\";\n          label.style.opacity = String(shown * fade);\n          label.style.transform = `translate3d(${Math.round(column + 8 * (shown - 1))}px, ${Math.round(top)}px, 0)`;\n          const desc = descEls.current[i];\n          if (desc) {\n            desc.style.opacity = String(open);\n            desc.style.transform = `translateY(${(open - 1) * 4}px)`;\n          }\n        }\n        if (line) {\n          line.style.opacity = String(fade);\n          line.setAttribute(\n            \"points\",\n            leader(\n              [anchor, { x: column - LABEL.knee, y: anchor.y }, { x: column - 6, y }],\n              drawn,\n            ),\n          );\n        }\n        if (dot) {\n          dot.style.opacity = String(fade * Math.min(1, drawn * 3));\n          dot.style.transform = `translate(${anchor.x}px, ${anchor.y}px) scale(${Math.min(1, drawn * 3) * (1 + 0.5 * open)})`;\n        }\n      }\n    };\n\n    const schedule = () => frame.render(update);\n    measure();\n\n    const unsubscribe = [progress, turnX, turnY, ...focus.flatMap((f) => [f.lift, f.dim, f.ghost])].map(\n      (value: MotionValue<number>) => value.on(\"change\", schedule),\n    );\n    const observer = new ResizeObserver(measure);\n    const watched = [\n      stage,\n      plane,\n      listRef.current,\n      ...layerEls.current,\n      ...titleEls.current,\n      ...descEls.current,\n    ];\n    for (const el of watched) if (el) observer.observe(el);\n\n    return () => {\n      unsubscribe.forEach((stop) => stop());\n      observer.disconnect();\n      cancelFrame(update);\n    };\n  }, [\n    back,\n    bareKey,\n    count,\n    end,\n    focus,\n    labeledKey,\n    labels,\n    mode,\n    progress,\n    spread,\n    still,\n    turn,\n    turnX,\n    turnY,\n    walkthrough,\n  ]);\n\n  // The pointer turns the open stack, and hovering a layer highlights it.\n  const onPointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n    if (!interactive || event.pointerType !== \"mouse\") return;\n    const stage = stageRef.current;\n    const g = geoRef.current;\n    const live = liveRef.current;\n    if (!stage || !g || !live) return;\n    const box = stage.getBoundingClientRect();\n    const x = event.clientX - box.left;\n    const y = event.clientY - box.top;\n    if (!still) {\n      turnY.set((x / box.width - 0.5) * 2 * EXPLODE.pointer);\n      turnX.set((0.5 - y / box.height) * 2 * EXPLODE.pointer);\n    }\n    if (!live.open) return;\n    // Hit-tested where the layers rest, without their hover lift, so a layer\n    // rising never slips out from under the pointer and drops back.\n    const project = projector(live.pose, g);\n    let hit: number | null = null;\n    for (let i = count - 1; i >= 0; i--) {\n      const b = g.boxes[i];\n      const corners = [\n        [b.x, b.y],\n        [b.x + b.w, b.y],\n        [b.x + b.w, b.y + b.h],\n        [b.x, b.y + b.h],\n      ].map(([cx, cy]) => {\n        const p = project(cx, cy, live.heights[i]);\n        return { x: p.x + g.plane.x, y: p.y + g.plane.y };\n      });\n      if (inside({ x, y }, corners)) {\n        hit = i;\n        break;\n      }\n    }\n    setHovered(hit);\n  };\n\n  const onPointerLeave = () => {\n    turnX.set(0);\n    turnY.set(0);\n    setHovered(null);\n  };\n\n  /** Scrolls the walkthrough to a layer, or pins it when there's no walkthrough to scroll. */\n  const choose = (i: number) => {\n    const section = sectionRef.current;\n    if (!liveRef.current?.open) return;\n    if (!walkthrough || still || !section) {\n      setPinned((current) => (current === i ? null : i));\n      return;\n    }\n    const range = end === \"assembled\" ? BEATS.tour : OPEN_TOUR;\n    const slot = labeled.indexOf(i);\n    const at = range[0] + ((slot + 0.5) / labeled.length) * (range[1] - range[0]);\n    const scroller = containerRef?.current;\n    const top = section.getBoundingClientRect().top - (scroller?.getBoundingClientRect().top ?? 0);\n    const offset = scroller ? scroller.scrollTop : window.scrollY;\n    const view = scroller ? scroller.clientHeight : window.innerHeight;\n    const target = offset + top + at * (section.offsetHeight - view);\n    (scroller ?? window).scrollTo({ top: target, behavior: \"smooth\" });\n  };\n\n  const labelHandlers = (i: number) =>\n    interactive\n      ? {\n          onPointerEnter: () => {\n            if (liveRef.current?.open) setPointed(i);\n          },\n          onPointerLeave: () => setPointed((current) => (current === i ? null : current)),\n          onFocus: () => {\n            if (liveRef.current?.open) setPointed(i);\n          },\n          onBlur: () => setPointed((current) => (current === i ? null : current)),\n          onClick: () => choose(i),\n        }\n      : {};\n\n  return (\n    <section ref={sectionRef} className=\"relative\" {...props}>\n      <div\n        ref={stageRef}\n        onPointerMove={onPointerMove}\n        onPointerLeave={onPointerLeave}\n        className={cn(\n          \"relative flex h-dvh w-full items-center justify-center overflow-hidden px-4 sm:px-6\",\n          !still && \"sticky top-0\",\n          stageClassName,\n        )}\n      >\n        <div\n          ref={planeRef}\n          className={cn(\"@container relative aspect-[16/10] w-full max-w-5xl\", className)}\n        >\n          {/* Nothing in the stack is interactive: it's a picture of the product. */}\n          <div ref={rigRef} inert className=\"absolute inset-0 [transform-style:preserve-3d]\">\n            <div ref={floorRef} aria-hidden className=\"pointer-events-none invisible absolute inset-0 opacity-0\">\n              <div className=\"absolute -inset-[30%] [--grid:rgb(9_9_11/0.09)] [background-image:linear-gradient(to_right,var(--grid)_1px,transparent_1px),linear-gradient(to_bottom,var(--grid)_1px,transparent_1px)] [background-position:center] [background-size:5cqw_5cqw] [mask-image:radial-gradient(closest-side,#000_55%,transparent)] dark:[--grid:rgb(255_255_255/0.08)]\" />\n              <div\n                ref={floorShadowRef}\n                className=\"absolute inset-[4%] rounded-[8%] bg-zinc-950/15 opacity-0 dark:bg-black/60\"\n              />\n            </div>\n\n            {layers.map((layer, i) => {\n              const { className: placement, bare, children: content } = layer.props;\n              return (\n                <div\n                  key={layer.key ?? i}\n                  ref={(el) => {\n                    layerEls.current[i] = el;\n                  }}\n                  className={cn(\n                    \"absolute\",\n                    !(placement && PLACED.test(placement)) && \"inset-0\",\n                    !bare && \"overflow-hidden rounded-lg\",\n                    placement,\n                  )}\n                >\n                  {content}\n                  {/* Shadows cast on this sheet by the layers above it. */}\n                  {!bare\n                    ? layers.slice(i + 1).map((above, k) =>\n                        above.props.bare\n                          ? null\n                          : ([\"contact\", \"soft\"] as const).map((kind) => (\n                              <span\n                                key={`${i + 1 + k}:${kind}`}\n                                ref={(el) => {\n                                  const key = `${i}:${i + 1 + k}:${kind}`;\n                                  if (el) shadowEls.current.set(key, el);\n                                  else shadowEls.current.delete(key);\n                                }}\n                                aria-hidden\n                                className=\"pointer-events-none absolute rounded-[inherit] bg-zinc-950 opacity-0 will-change-[opacity,transform] dark:bg-black\"\n                              />\n                            )),\n                      )\n                    : null}\n                  {/* The sheet's edge and the light along its top, once it's off the page. */}\n                  {!bare ? (\n                    <span\n                      ref={(el) => {\n                        edgeEls.current[i] = el;\n                      }}\n                      aria-hidden\n                      className=\"pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 ring-1 ring-zinc-950/10 will-change-[opacity] ring-inset dark:ring-white/15\"\n                    >\n                      <span className=\"absolute inset-x-[6%] top-0 h-px bg-linear-to-r from-transparent via-white to-transparent dark:via-white/40\" />\n                    </span>\n                  ) : null}\n                </div>\n              );\n            })}\n\n            {/* What's left of a layer above the active one: its edge. */}\n            {layers.map((layer, i) =>\n              layer.props.bare ? null : (\n                <span\n                  key={`outline-${layer.key ?? i}`}\n                  ref={(el) => {\n                    outlineEls.current[i] = el;\n                  }}\n                  aria-hidden\n                  className=\"pointer-events-none absolute top-0 left-0 border border-zinc-950/25 opacity-0 will-change-[opacity] dark:border-white/25\"\n                />\n              ),\n            )}\n          </div>\n        </div>\n\n        {mode === \"side\" ? (\n          <>\n            <svg aria-hidden className=\"pointer-events-none absolute inset-0 size-full overflow-visible text-zinc-400 dark:text-zinc-500\">\n              {labeled.map((i) => (\n                <g key={i}>\n                  <polyline\n                    ref={(el) => {\n                      lineEls.current[i] = el;\n                    }}\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth={1.5}\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeDasharray=\"0 4.5\"\n                  />\n                  <circle\n                    ref={(el) => {\n                      dotEls.current[i] = el;\n                    }}\n                    r={3}\n                    className=\"fill-zinc-950 opacity-0 [transform-box:fill-box] [transform-origin:center] dark:fill-zinc-50\"\n                  />\n                </g>\n              ))}\n            </svg>\n            <ol className=\"pointer-events-none absolute inset-0\">\n              {labeled.map((i) => {\n                const { label, description } = layers[i].props;\n                const Tag = interactive ? \"button\" : \"div\";\n                return (\n                  <li\n                    key={i}\n                    ref={(el) => {\n                      labelEls.current[i] = el;\n                    }}\n                    className=\"invisible absolute top-0 left-0 opacity-0 will-change-[transform,opacity]\"\n                  >\n                    <Tag\n                      {...(interactive ? { type: \"button\" as const } : {})}\n                      {...labelHandlers(i)}\n                      className={cn(\n                        \"relative -mx-2 -my-1 block rounded-md px-2 py-1 text-left outline-none\",\n                        interactive && \"pointer-events-auto cursor-pointer focus-visible:ring-2 focus-visible:ring-zinc-950/15 dark:focus-visible:ring-white/20\",\n                      )}\n                    >\n                      <span\n                        ref={(el) => {\n                          titleEls.current[i] = el;\n                        }}\n                        className=\"block text-sm font-medium whitespace-nowrap text-zinc-950 dark:text-zinc-50\"\n                      >\n                        {label}\n                      </span>\n                      {description ? (\n                        <span\n                          ref={(el) => {\n                            descEls.current[i] = el;\n                          }}\n                          className=\"absolute top-full left-2 block w-max max-w-[15rem] text-[13px] leading-snug text-pretty text-zinc-500 opacity-0 dark:text-zinc-400\"\n                        >\n                          {description}\n                        </span>\n                      ) : null}\n                    </Tag>\n                  </li>\n                );\n              })}\n            </ol>\n          </>\n        ) : null}\n\n        {mode === \"numbered\" ? (\n          <>\n            {labeled.map((i, n) => (\n              <span\n                key={i}\n                ref={(el) => {\n                  markerEls.current[i] = el;\n                }}\n                aria-hidden\n                className=\"pointer-events-none invisible absolute top-0 left-0 grid size-5 place-items-center rounded-full bg-zinc-950 text-[11px] font-semibold text-white tabular-nums opacity-0 shadow-sm will-change-[transform,opacity] dark:bg-white dark:text-zinc-950\"\n              >\n                {n + 1}\n              </span>\n            ))}\n            <div\n              ref={listRef}\n              className=\"pointer-events-none absolute inset-x-0 top-0 flex flex-col items-center gap-2 px-4\"\n            >\n              <ol className=\"flex flex-wrap justify-center gap-1\">\n                {labeled.map((i, n) => {\n                  const { label } = layers[i].props;\n                  const Tag = interactive ? \"button\" : \"div\";\n                  return (\n                    <li\n                      key={i}\n                      ref={(el) => {\n                        labelEls.current[i] = el;\n                      }}\n                      className=\"invisible opacity-0\"\n                    >\n                      <Tag\n                        {...(interactive ? { type: \"button\" as const } : {})}\n                        {...labelHandlers(i)}\n                        className={cn(\n                          \"flex items-center gap-1.5 rounded-full py-1 pr-2.5 pl-1 text-[13px] font-medium whitespace-nowrap text-zinc-500 transition-colors duration-200 dark:text-zinc-400\",\n                          active === i && \"bg-zinc-950/5 text-zinc-950 dark:bg-white/10 dark:text-zinc-50\",\n                          interactive && \"pointer-events-auto cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-zinc-950/15 dark:focus-visible:ring-white/20\",\n                        )}\n                      >\n                        <span className=\"grid size-4.5 place-items-center rounded-full bg-zinc-950/10 text-[10px] font-semibold tabular-nums dark:bg-white/15\">\n                          {n + 1}\n                        </span>\n                        {label}\n                      </Tag>\n                    </li>\n                  );\n                })}\n              </ol>\n              <div aria-live=\"polite\" className=\"grid min-h-[2lh] max-w-sm text-center text-[13px] leading-snug text-pretty text-zinc-500 dark:text-zinc-400\">\n                {labeled.map((i) => (\n                  <p\n                    key={i}\n                    className={cn(\n                      \"col-start-1 row-start-1 transition-opacity duration-200\",\n                      active === i ? \"opacity-100\" : \"opacity-0\",\n                    )}\n                  >\n                    {layers[i].props.description}\n                  </p>\n                ))}\n              </div>\n            </div>\n          </>\n        ) : null}\n      </div>\n      {/* Scroll runway: the stage stays stuck while this scrolls past. */}\n      {!still ? (\n        <div\n          aria-hidden\n          style={{ height: stageHeight ? stageHeight * scrollLength : `${scrollLength * 100}dvh` }}\n        />\n      ) : null}\n    </section>\n  );\n}\n\n/**\n * A product screenshot that comes apart into its layers as you scroll,\n * explains each one, then clicks back together.\n */\nexport const ExplodedUI = Object.assign(ExplodedUIRoot, { Layer: ExplodedUILayer });\n\n/** The rig's transform. `projector` below is the same thing, as math. */\nfunction rigTransform(pose: Pose) {\n  return (\n    `translate3d(${pose.tx}px, ${pose.ty}px, 0) rotateX(${pose.px}deg) rotateY(${pose.py}deg) ` +\n    `scale3d(${pose.scale}, ${pose.scale}, ${pose.scale}) rotateX(${pose.back}deg) ` +\n    `rotateZ(${pose.turn}deg) translateZ(${-pose.sink}px)`\n  );\n}\n\n/**\n * Where a point on the plane lands on screen under a pose: the rig's\n * transform, then the plane's perspective. Takes plane coordinates and a\n * layer's height; returns plane coordinates, flat.\n */\nfunction projector(pose: Pose, g: Pick<Geometry, \"plane\" | \"depth\">) {\n  const rad = Math.PI / 180;\n  const sb = Math.sin(pose.back * rad);\n  const cb = Math.cos(pose.back * rad);\n  const st = Math.sin(pose.turn * rad);\n  const ct = Math.cos(pose.turn * rad);\n  const sx = Math.sin(pose.px * rad);\n  const cx = Math.cos(pose.px * rad);\n  const sy = Math.sin(pose.py * rad);\n  const cy = Math.cos(pose.py * rad);\n  const ox = g.plane.w / 2;\n  const oy = g.plane.h / 2;\n  return (x: number, y: number, z: number): Point => {\n    let X = x - ox;\n    let Y = y - oy;\n    let Z = z - pose.sink;\n    let t = X * ct - Y * st; // rotateZ(turn)\n    Y = X * st + Y * ct;\n    X = t;\n    t = Y * cb - Z * sb; // rotateX(back)\n    Z = Y * sb + Z * cb;\n    Y = t;\n    X *= pose.scale;\n    Y *= pose.scale;\n    Z *= pose.scale;\n    t = X * cy + Z * sy; // rotateY(py)\n    Z = -X * sy + Z * cy;\n    X = t;\n    t = Y * cx - Z * sx; // rotateX(px)\n    Z = Y * sx + Z * cx;\n    Y = t;\n    X += pose.tx;\n    Y += pose.ty;\n    const k = g.depth / (g.depth - Z);\n    return { x: ox + X * k, y: oy + Y * k };\n  };\n}\n\n/**\n * Scales and centers the open stack so it fits the stage with its labels,\n * and picks where each label attaches. Side labels attach at the corner of\n * the layer nearest them; numbered markers sit on the layer's front edge,\n * the part of each sheet that stays in view.\n */\nfunction fitStack(g: Geometry, room: { kind: \"side\" | \"numbered\"; side: number; below: number; marked: number[] }) {\n  const n = g.boxes.length;\n  const pose: Pose = {\n    back: g.back,\n    turn: g.turn,\n    px: 0,\n    py: 0,\n    scale: 1,\n    tx: 0,\n    ty: 0,\n    sink: ((n - 1) * g.gap) / 2,\n  };\n  const lift = EXPLODE.lift * g.gap;\n  const below = room.below ? room.below + LABEL.pad : 0;\n  const availW = g.stage.w - LABEL.pad * 2;\n  const availH = g.stage.h - LABEL.pad * 2 - below;\n\n  const rest = projector(pose, g);\n  g.anchors = g.boxes.map((b, i) => {\n    const inset = Math.min(10, b.w * 0.08, b.h * 0.08);\n    const points =\n      room.kind === \"side\"\n        ? [\n            [b.x + inset, b.y + inset],\n            [b.x + b.w - inset, b.y + inset],\n            [b.x + b.w - inset, b.y + b.h - inset],\n            [b.x + inset, b.y + b.h - inset],\n          ]\n        : [\n            [b.x + b.w / 2, b.y + inset * 2],\n            [b.x + b.w - inset * 2, b.y + b.h / 2],\n            [b.x + b.w / 2, b.y + b.h - inset * 2],\n            [b.x + inset * 2, b.y + b.h / 2],\n          ];\n    // Side: the furthest right. Numbered: the lowest on screen, so the front.\n    const score = ([x, y]: number[]) => {\n      const p = rest(x, y, i * g.gap);\n      return room.kind === \"side\" ? p.x : p.y;\n    };\n    const [x, y] = points.reduce((best, point) => (score(point) > score(best) ? point : best));\n    return { x, y };\n  });\n  g.order = [...room.marked].sort(\n    (a, b) =>\n      rest(g.anchors[a].x, g.anchors[a].y, a * g.gap).y - rest(g.anchors[b].x, g.anchors[b].y, b * g.gap).y,\n  );\n\n  for (let pass = 0; pass < 3; pass++) {\n    const project = projector(pose, g);\n    let minX = Infinity;\n    let maxX = -Infinity;\n    let minY = Infinity;\n    let maxY = -Infinity;\n    g.boxes.forEach((b, i) => {\n      // Where it rests, and raised as the active layer.\n      for (const z of [i * g.gap, i * g.gap + lift]) {\n        for (const [x, y] of [\n          [b.x, b.y],\n          [b.x + b.w, b.y],\n          [b.x + b.w, b.y + b.h],\n          [b.x, b.y + b.h],\n        ]) {\n          const p = project(x, y, z);\n          minX = Math.min(minX, p.x);\n          maxX = Math.max(maxX, p.x);\n          minY = Math.min(minY, p.y);\n          maxY = Math.max(maxY, p.y);\n        }\n      }\n    });\n    let reach = -Infinity;\n    for (const i of room.marked) {\n      reach = Math.max(reach, project(g.anchors[i].x, g.anchors[i].y, i * g.gap).x);\n    }\n    const right = room.side ? Math.max(maxX, reach + room.side) : maxX;\n\n    if (pass === 0) {\n      pose.scale = Math.max(\n        0.3,\n        Math.min(\n          EXPLODE.scale,\n          availW / (maxX - minX),\n          room.side ? (availW - room.side) / (reach - minX) : Infinity,\n          availH / (maxY - minY),\n        ),\n      );\n      continue;\n    }\n    // Center the stack, with its labels beside it or its list under it.\n    const shift = g.stage.h / 2 - g.plane.y - (minY + maxY + below) / 2;\n    pose.tx += g.stage.w / 2 - g.plane.x - (minX + right) / 2;\n    pose.ty += shift;\n    g.listTop = g.plane.y + maxY + shift + LABEL.pad;\n  }\n  g.fit = { scale: pose.scale, tx: pose.tx, ty: pose.ty };\n}\n\n/** How open each gap is, 0 to 1. gaps[i] is the one under layer i, so gaps[0] stays 0. */\nfunction openGaps(p: number, end: \"assembled\" | \"exploded\", gaps: number[]) {\n  const m = gaps.length - 1;\n  if (m < 1) return;\n  // Each gap gets `span` of the beat, starting `step` after the one above it.\n  const span = 1 / (1 + (m - 1) * (1 - EXPLODE.overlap));\n  const step = span * (1 - EXPLODE.overlap);\n  const opening = segment(p, BEATS.explode);\n  const closing = end === \"assembled\" ? segment(p, BEATS.collapse) : 0;\n  for (let k = 1; k <= m; k++) {\n    const opened = EXPLODE.ease(clamp01((opening - (m - k) * step) / span)); // top first\n    const closed = EXPLODE.ease(clamp01((closing - (k - 1) * step) / span)); // bottom first\n    gaps[k] = opened * (1 - closed);\n  }\n}\n\n/**\n * Spaces labels so none overlap, moving each as little as it can. An open\n * label weighs more, so its neighbors make room around it.\n */\nfunction stack(items: { top: number; height: number; weight: number }[], min: number, max: number) {\n  for (let pass = 0; pass < 6; pass++) {\n    for (let k = 1; k < items.length; k++) {\n      const a = items[k - 1];\n      const b = items[k];\n      const overlap = a.top + a.height + LABEL.gap - b.top;\n      if (overlap <= 0) continue;\n      const share = b.weight / (a.weight + b.weight);\n      a.top -= overlap * share;\n      b.top += overlap * (1 - share);\n    }\n  }\n  // Then keep them apart, and on the stage.\n  for (let k = 0; k < items.length; k++) {\n    const floor = k ? items[k - 1].top + items[k - 1].height + LABEL.gap : min;\n    items[k].top = Math.max(items[k].top, floor);\n  }\n  for (let k = items.length - 1; k >= 0; k--) {\n    const ceiling = k < items.length - 1 ? items[k + 1].top - LABEL.gap : max;\n    items[k].top = Math.max(min, Math.min(items[k].top, ceiling - items[k].height));\n  }\n}\n\n/** The first `drawn` share of a leader, as SVG points. */\nfunction leader(points: Point[], drawn: number) {\n  const lengths = points.slice(1).map((p, i) => Math.hypot(p.x - points[i].x, p.y - points[i].y));\n  let left = drawn * lengths.reduce((sum, l) => sum + l, 0);\n  const out = [points[0]];\n  for (let i = 1; i < points.length && left > 0; i++) {\n    const l = lengths[i - 1];\n    const f = l > 0 ? Math.min(1, left / l) : 1;\n    out.push({\n      x: points[i - 1].x + (points[i].x - points[i - 1].x) * f,\n      y: points[i - 1].y + (points[i].y - points[i - 1].y) * f,\n    });\n    left -= l;\n  }\n  return out.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(\" \");\n}\n\n/** Whether a point is inside a convex quad. */\nfunction inside(p: Point, quad: Point[]) {\n  let sign = 0;\n  for (let i = 0; i < quad.length; i++) {\n    const a = quad[i];\n    const b = quad[(i + 1) % quad.length];\n    const cross = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);\n    if (cross === 0) continue;\n    if (sign === 0) sign = Math.sign(cross);\n    else if (Math.sign(cross) !== sign) return false;\n  }\n  return true;\n}\n\n/** How far a layer has faded back, 0 to 1, whichever side of the active one it's on. */\nfunction dimmed(fade: number) {\n  return Math.min(1, fade / EXPLODE.fade.below);\n}\n\nfunction segment(p: number, [a, b]: readonly [number, number]) {\n  return clamp01((p - a) / (b - a));\n}\n\nfunction clamp01(v: number) {\n  return v < 0 ? 0 : v > 1 ? 1 : v;\n}\n\nfunction smooth(a: number, b: number, v: number) {\n  const x = clamp01((v - a) / (b - a));\n  return x * x * (3 - 2 * x);\n}\n\nconst REDUCED_MOTION = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribeToReducedMotion(onChange: () => void) {\n  const query = window.matchMedia(REDUCED_MOTION);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n}\n\n/** Reads false on the server and while hydrating, so the markup always matches. */\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeToReducedMotion,\n    () => window.matchMedia(REDUCED_MOTION).matches,\n    () => false,\n  );\n}\n"
    }
  ]
}