{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "launch-dock",
  "type": "registry:ui",
  "title": "Launch Dock",
  "description": "A dock of app icons under a window. Pick one and its window grows up out of the icon in 0.3s, over the open one, which dissolves where it is. One label glides over the dock as you point, and with autoplay the dot under the open app fills like a timer before the next one launches.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "showcase",
    "testimonials",
    "motion"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/launch-dock",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/launch-dock.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  animate,\n  motion,\n  useInView,\n  useMotionValue,\n  useReducedMotion,\n  type AnimationPlaybackControls,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - Windows animate only transform and opacity, as hardware animations on their own layers, and\n *   closing animates opacity alone; nothing repaints mid-flight.\n * - The open window never moves: it dissolves in place under the new one (z-index 2 over 1), which\n *   is opaque from its first frame behind the icon, so two windows never cross or blend.\n * - A pick measures its icon before any state changes, so the launch's first frame never waits on\n *   layout.\n * - A launch turns round from wherever it is: a window still dissolving comes back in place instead\n *   of growing from its icon again.\n * - Closed windows stay mounted, `hidden` and `inert`, as tabpanels of a tablist with a roving\n *   `tabIndex`, so their content and state survive a switch.\n */\nconst TIMING = {\n  /** A window growing up out of its icon, in seconds (the `duration` prop) and its curve. */\n  launch: { duration: 0.3, ease: [0.22, 1, 0.36, 1] },\n  /** The open one fading where it is, under the new one, at the default `duration`. */\n  dissolve: { duration: 0.22, ease: \"linear\" },\n  /** An icon rising under the pointer. */\n  lift: { type: \"spring\", visualDuration: 0.25, bounce: 0.2 },\n  /** And dipping while it's pressed. */\n  press: { type: \"spring\", visualDuration: 0.12, bounce: 0 },\n  /** The label and the dot moving between icons. */\n  glide: { type: \"spring\", visualDuration: 0.26, bounce: 0 },\n  /** The label appearing, in seconds. */\n  show: { duration: 0.15, ease: [0.22, 1, 0.36, 1] },\n  /** And going, in seconds. */\n  hide: { duration: 0.12, ease: [0.22, 1, 0.36, 1] },\n  /** Windows crossfading under reduced motion, in seconds. */\n  fade: { duration: 0.2, ease: [0.22, 1, 0.36, 1] },\n  /** The label waiting after the pointer leaves an icon, in ms. */\n  leave: 120,\n  /** The label staying over a tapped icon, in ms. */\n  touchLabel: 1200,\n  /** Each app staying up when `autoplay` is `true`, in ms. */\n  autoplay: 4500,\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * LAUNCH DOCK STORYBOARD\n *\n *  rest      a window over a dock of rounded icons. The open app's\n *            window sits under the dock with its lower edge fading out\n *            behind it, and a 4px dot marks its icon\n *  hover     under a mouse the icon you're on rises 6px and grows to\n *            1.1 on a quick spring with a little give. One label hangs\n *            over the dock and glides from icon to icon, its width\n *            following the name; it shows at once and waits 120ms\n *            before it goes\n *  launch    pick an icon and, on the same frame, its window starts\n *            growing up out of it: from a copy small enough to hide\n *            behind the icon to full size in 0.3s on a strong ease-out,\n *            so most of the growth is over in the first 0.1s. It's\n *            opaque from the first frame (it starts hidden behind the\n *            icon), so two apps never blend, and the dot slides across\n *  dissolve  the open window doesn't move: it dissolves where it is\n *            (0.22s) while the new one grows over it, so nothing crosses\n *            and the stage is never empty\n *  smooth    windows move by transform and opacity only, as hardware\n *            animations on their own layers: nothing repaints while\n *            they fly, and a busy page can't stall them. The icons are\n *            measured at the click, before anything else happens\n *  autoplay  with autoplay the dot stretches into a 14px bar that fills\n *            while an app is up (4.5s each), then the next one\n *            launches. A pointer over the dock, focus inside it, a\n *            hidden tab or the dock out of view pauses it; picking an\n *            app stops it for good\n *  again     a launch turns round from wherever it is: click back fast\n *            and a dissolving window comes back in place, never queues\n *  touch     a tap launches, and the label shows over the tapped icon\n *            for 1.2s, since there's no hover to show it\n *  keys      the dock is a tablist. Tab lands on the open app, the\n *            arrows move along and launch, Home and End jump to the\n *            ends, and Tab carries on into the window, its tabpanel\n *  reduced   nothing flies or rises: windows crossfade in 0.2s, the\n *            label fades, and autoplay stays off\n * ───────────────────────────────────────────────────────── */\n\nconst DOCK = {\n  tuck: 0.9, // share of the icon a window's smallest copy fills, so it hides behind it\n  rise: 6, // px an icon rises under the pointer\n  grow: 1.1, // scale it grows to\n  dip: 0.92, // scale while it's pressed\n  feather: 0.28, // share of the window's height that fades out behind the dock\n  dot: 4, // px; the open app's dot\n  bar: 14, // px; the dot while autoplay counts down\n  labelPad: 20, // px of padding around the label's text\n} as const;\n\n// The window's corners at rest, per frame.\nconst RADIUS = { window: 12, card: 20, none: 0 } as const;\nconst AT_REST = \"translate(0px, 0px) scale(1)\";\nconst DEFAULT_SIZE: [number, number] = [960, 600];\n\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\nexport interface LaunchDockItem {\n  /** A unique id. `value` and `onValueChange` use it. */\n  id: string;\n  /** The app's name: the label over the dock, the window's title and the icon's accessible name. */\n  label: string;\n  /** The dock icon. It's clipped to a rounded square, so a full-bleed `<img>` or SVG tile both work. */\n  icon: React.ReactNode;\n  /** What the window shows. It's laid out at `windowSize` and scaled down to fit, like a screenshot. */\n  content: React.ReactNode;\n  /** The window's title bar text, when it should say more than the label: a domain, a file name. */\n  title?: React.ReactNode;\n}\n\nexport interface LaunchDockProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\" | \"children\"> {\n  /** The apps, in dock order. */\n  items: LaunchDockItem[];\n  /** The open app's id, when you control it. */\n  value?: string;\n  /** The app that's open at first. Defaults to the first one. */\n  defaultValue?: string;\n  /** Called with an app's id whenever it launches, and why: the visitor picked it, or autoplay moved on. */\n  onValueChange?: (id: string, reason: \"pick\" | \"autoplay\") => void;\n  /** Launch the next app on its own every 4.5s until the visitor picks one. A number sets the time in ms. */\n  autoplay?: boolean | number;\n  /** The size each window's content is laid out at, in px. Narrower containers scale it down to fit. */\n  windowSize?: [number, number];\n  /** What the content sits in: a desktop window with a title bar, a plain card, or nothing at all. */\n  frame?: \"window\" | \"card\" | \"none\";\n  /** Fade the window's lower edge out behind the dock. Turn it off to keep all of the content in view; the dock then sits under the window. */\n  fade?: boolean;\n  /** Seconds a window takes to grow up out of its icon. The open one dissolves in proportion, so it's gone just before the new one lands. */\n  duration?: number;\n  /** The scale an icon grows to under the pointer. `1` keeps it still but for the rise. */\n  hoverScale?: number;\n  /** The dock's accessible name. */\n  dockLabel?: string;\n  /** Classes for the dock's surface: its fill, ring and shadow. */\n  trayClassName?: string;\n  /** Classes for each window's surface: its fill, ring and corners. */\n  windowClassName?: string;\n}\n\n/** Where a window hides behind its icon, as the transform that puts it there (origin top left). */\ntype Docked = string;\n\n/* ── TitleBar ───────────────────────────────────────────── */\n\n// The window's chrome: three lights and a centered title. Purely drawn; the lights don't act.\nconst BAR = \"h-7 @[30rem]/launch-dock:h-9\";\n\nfunction TitleBar({ title }: { title: React.ReactNode }) {\n  return (\n    <div\n      className={cn(\n        BAR,\n        \"relative flex shrink-0 items-center border-b border-zinc-950/[0.06] bg-zinc-50/90 px-3 dark:border-white/[0.06] dark:bg-white/[0.04] @[30rem]/launch-dock:px-3.5\",\n      )}\n    >\n      <div aria-hidden className=\"flex gap-1.5 @[30rem]/launch-dock:gap-2\">\n        <span className=\"size-2.5 rounded-full bg-[#ff5f57] ring-1 ring-black/10 ring-inset @[30rem]/launch-dock:size-3\" />\n        <span className=\"size-2.5 rounded-full bg-[#febc2e] ring-1 ring-black/10 ring-inset @[30rem]/launch-dock:size-3\" />\n        <span className=\"size-2.5 rounded-full bg-[#28c840] ring-1 ring-black/10 ring-inset @[30rem]/launch-dock:size-3\" />\n      </div>\n      <div className=\"pointer-events-none absolute inset-x-16 truncate text-center text-[11px] font-medium text-zinc-500 @[30rem]/launch-dock:inset-x-20 @[30rem]/launch-dock:text-xs dark:text-zinc-400\">\n        {title}\n      </div>\n    </div>\n  );\n}\n\n/* ── DockWindow ─────────────────────────────────────────── */\n\n// One app's window. It stays mounted, so its images load early and its state survives, and is\n// hidden while closed. Opening animates only its transform and opacity, closing only its opacity,\n// and both run as hardware animations: the window is rastered once, never repainted mid-flight.\nconst DockWindow = React.memo(function DockWindow({\n  item,\n  open,\n  frame,\n  fade,\n  reduced,\n  duration,\n  docked,\n  width,\n  height,\n  tabId,\n  panelId,\n  className,\n}: {\n  item: LaunchDockItem;\n  open: boolean;\n  frame: \"window\" | \"card\" | \"none\";\n  fade: boolean;\n  reduced: boolean;\n  duration: number;\n  docked: (id: string) => Docked | null;\n  width: number;\n  height: number;\n  tabId: string;\n  panelId: string;\n  className?: string;\n}) {\n  const [present, setPresent] = React.useState(open);\n  if (open && !present) setPresent(true);\n\n  const outerRef = React.useRef<HTMLDivElement>(null);\n  const flight = React.useRef<AnimationPlaybackControls | null>(null);\n  // Settled (\"open\"/\"closed\") or on its way; a window still on its way turns round from where it is.\n  const phase = React.useRef<\"open\" | \"closed\" | \"opening\" | \"closing\">(open ? \"open\" : \"closed\");\n  // The `open` the window last moved for. Strict Mode replays effects on mount; this keeps that\n  // replay (and any re-render that doesn't change `open`) from starting a flight.\n  const shown = React.useRef(open);\n  const rest = RADIUS[frame];\n\n  React.useLayoutEffect(() => {\n    if (shown.current === open) return;\n    shown.current = open;\n    const outer = outerRef.current;\n    if (!outer) return;\n    flight.current?.stop();\n    const settled = phase.current === \"open\" || phase.current === \"closed\";\n    phase.current = open ? \"opening\" : \"closing\";\n    const done = () => {\n      phase.current = open ? \"open\" : \"closed\";\n      if (!open) setPresent(false);\n    };\n\n    const target = reduced || !open ? null : docked(item.id);\n    if (!target && (open || reduced)) {\n      flight.current = animate(\n        outer,\n        { transform: AT_REST, opacity: open ? [0, 1] : 0 },\n        { ...TIMING.fade, transform: { duration: 0 }, onComplete: done },\n      );\n    } else if (open) {\n      // From behind the icon on a fresh launch; from wherever it is if it was still leaving.\n      flight.current = animate(\n        outer,\n        settled ? { transform: [target, AT_REST], opacity: 1 } : { transform: AT_REST, opacity: 1 },\n        { ...TIMING.launch, duration, opacity: { duration: settled ? 0 : TIMING.fade.duration }, onComplete: done },\n      );\n    } else {\n      // It stays put and dissolves under the window growing over it, so nothing crosses.\n      const dissolve = TIMING.dissolve.duration * (duration / TIMING.launch.duration);\n      flight.current = animate(outer, { opacity: 0 }, { ...TIMING.dissolve, duration: dissolve, onComplete: done });\n    }\n  }, [open, reduced, docked, item.id, duration]);\n\n  React.useEffect(() => () => flight.current?.stop(), []);\n\n  return (\n    <div\n      ref={outerRef}\n      id={panelId}\n      role=\"tabpanel\"\n      aria-labelledby={tabId}\n      hidden={!present}\n      inert={!open}\n      data-launch-dock-window={item.id}\n      data-state={open ? \"open\" : \"closed\"}\n      style={{ zIndex: open ? 2 : 1 }}\n      className=\"absolute inset-x-0 top-0 origin-top-left [will-change:transform,opacity]\"\n    >\n      {frame !== \"none\" ? (\n        <div\n          aria-hidden\n          style={{ bottom: fade ? `${DOCK.feather * 100 + 8}%` : 0, borderRadius: rest }}\n          className=\"pointer-events-none absolute inset-x-0 top-0 shadow-[0_24px_60px_-18px_rgb(0_0_0/0.3),0_8px_20px_-10px_rgb(0_0_0/0.18)] dark:shadow-[0_24px_60px_-18px_rgb(0_0_0/0.75),0_8px_20px_-10px_rgb(0_0_0/0.5)]\"\n        />\n      ) : null}\n      <div\n        style={{ borderRadius: rest }}\n        className={cn(\n          \"relative flex flex-col overflow-hidden\",\n          fade && \"[mask-image:linear-gradient(to_bottom,#000_72%,transparent)]\",\n          frame === \"window\" && \"bg-white ring-1 ring-zinc-950/10 ring-inset dark:bg-zinc-900 dark:ring-white/10\",\n          frame === \"card\" && \"bg-white ring-1 ring-zinc-950/[0.07] ring-inset dark:bg-zinc-900 dark:ring-white/10\",\n          className,\n        )}\n      >\n        {frame === \"window\" ? <TitleBar title={item.title ?? item.label} /> : null}\n        <div className=\"relative w-full overflow-hidden\" style={{ aspectRatio: `${width} / ${height}` }}>\n          <div\n            className=\"invisible absolute top-0 left-0 origin-top-left group-data-[measured]/launch-dock-stage:visible\"\n            style={{ width, height, transform: \"scale(var(--launch-dock-scale, 1))\" }}\n          >\n            {item.content}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n});\n\n/* ── DockTab ────────────────────────────────────────────── */\n\ntype TabEvent = \"down\" | \"up\" | \"enter\" | \"leave\" | \"focus\" | \"blur\" | \"click\";\n\n// One icon in the dock. It's memoized and hands every event to one stable callback, so a switch\n// re-renders only the icons whose state changed, not the whole dock.\nconst DockTab = React.memo(function DockTab({\n  item,\n  active,\n  lifted,\n  dipped,\n  grow,\n  tabId,\n  panelId,\n  register,\n  onTabEvent,\n}: {\n  item: LaunchDockItem;\n  active: boolean;\n  lifted: boolean;\n  dipped: boolean;\n  grow: number;\n  tabId: string;\n  panelId: string;\n  register: (kind: \"tab\" | \"tile\", id: string, node: HTMLElement | null) => void;\n  onTabEvent: (type: TabEvent, id: string, event: React.SyntheticEvent<HTMLElement>) => void;\n}) {\n  return (\n    <button\n      ref={(node) => register(\"tab\", item.id, node)}\n      type=\"button\"\n      role=\"tab\"\n      id={tabId}\n      aria-selected={active}\n      aria-controls={panelId}\n      aria-label={item.label}\n      tabIndex={active ? 0 : -1}\n      data-launch-dock-item={item.id}\n      data-active={active ? \"\" : undefined}\n      className=\"group/launch-dock-tab relative block shrink-0 cursor-pointer rounded-[var(--launch-dock-corner)] outline-none select-none [-webkit-tap-highlight-color:transparent] after:absolute after:-inset-1 after:content-['']\"\n      onPointerDown={(event) => onTabEvent(\"down\", item.id, event)}\n      onPointerUp={(event) => onTabEvent(\"up\", item.id, event)}\n      onPointerCancel={(event) => onTabEvent(\"up\", item.id, event)}\n      onPointerEnter={(event) => onTabEvent(\"enter\", item.id, event)}\n      onPointerLeave={(event) => onTabEvent(\"leave\", item.id, event)}\n      onFocus={(event) => onTabEvent(\"focus\", item.id, event)}\n      onBlur={(event) => onTabEvent(\"blur\", item.id, event)}\n      onClick={(event) => onTabEvent(\"click\", item.id, event)}\n    >\n      <motion.span\n        ref={(node) => register(\"tile\", item.id, node)}\n        initial={false}\n        animate={{\n          transform: `translateY(${lifted ? -DOCK.rise : 0}px) scale(${dipped ? DOCK.dip : lifted ? grow : 1})`,\n        }}\n        transition={dipped ? TIMING.press : TIMING.lift}\n        className=\"block size-[var(--launch-dock-icon)] origin-bottom overflow-hidden rounded-[var(--launch-dock-corner)] shadow-[0_1px_2px_rgb(0_0_0/0.12),0_4px_10px_-4px_rgb(0_0_0/0.25)] outline-offset-2 group-focus-visible/launch-dock-tab:outline-2 group-focus-visible/launch-dock-tab:outline-zinc-950 dark:group-focus-visible/launch-dock-tab:outline-white [&>img]:size-full [&>img]:object-cover [&>svg]:size-full\"\n      >\n        {item.icon}\n      </motion.span>\n    </button>\n  );\n});\n\n/* ── LaunchDock ─────────────────────────────────────────── */\n\nexport function LaunchDock({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  autoplay = false,\n  windowSize = DEFAULT_SIZE,\n  frame = \"window\",\n  fade = true,\n  duration = TIMING.launch.duration,\n  hoverScale = DOCK.grow,\n  dockLabel = \"Apps\",\n  trayClassName,\n  windowClassName,\n  className,\n  style,\n  onPointerEnter,\n  onPointerLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: LaunchDockProps) {\n  const reduced = useReducedMotion() ?? false;\n  const uid = React.useId();\n  const [width, height] = windowSize;\n\n  const [inner, setInner] = React.useState(defaultValue);\n  const requested = value ?? inner;\n  const active = items.some((item) => item.id === requested) ? requested : items[0]?.id;\n\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const stageRef = React.useRef<HTMLDivElement>(null);\n  const trayRef = React.useRef<HTMLDivElement>(null);\n  const tabs = React.useRef(new Map<string, HTMLElement>());\n  const tiles = React.useRef(new Map<string, HTMLElement>());\n  const labelWidths = React.useRef(new Map<string, number>());\n  const measurer = React.useRef<HTMLDivElement>(null);\n  const register = React.useCallback((kind: \"tab\" | \"tile\", id: string, node: HTMLElement | null) => {\n    const map = kind === \"tab\" ? tabs.current : tiles.current;\n    if (node) map.set(id, node);\n    else map.delete(id);\n  }, []);\n\n  // Hover lifts the icon under a mouse; the label follows hover, keyboard focus or a tap.\n  const [hover, setHover] = React.useState<string | null>(null);\n  const [pressed, setPressed] = React.useState<string | null>(null);\n  const [chip, setChip] = React.useState<{ id: string; on: boolean } | null>(null);\n  const leaveTimer = React.useRef<number | undefined>(undefined);\n  const flashTimer = React.useRef<number | undefined>(undefined);\n  const pointerType = React.useRef<string>(\"mouse\");\n\n  // Autoplay runs until the visitor picks something, and holds while they're looking closely.\n  const interval = autoplay === true ? TIMING.autoplay : typeof autoplay === \"number\" ? autoplay : 0;\n  const [stopped, setStopped] = React.useState(false);\n  const running = interval > 0 && !stopped && !reduced && items.length > 1;\n  const [hovering, setHovering] = React.useState(false);\n  const [focusWithin, setFocusWithin] = React.useState(false);\n  const [pageHidden, setPageHidden] = React.useState(false);\n  const inView = useInView(rootRef, { amount: 0.4 });\n  const paused = hovering || focusWithin || pageHidden || !inView;\n\n  const labelX = useMotionValue(0);\n  const labelW = useMotionValue(0);\n  const dotX = useMotionValue(0);\n  const dotW = useMotionValue<number>(DOCK.dot);\n  const fill = useMotionValue(1);\n  const elapsed = React.useRef(0);\n  const activeRef = React.useRef(active);\n  const placed = React.useRef({ label: false, dot: false });\n\n  React.useLayoutEffect(() => {\n    activeRef.current = active;\n  }, [active]);\n\n  // Where a window hides behind its icon, worked out from the icon as it sits right now (lifted,\n  // pressed). A pick measures it before anything changes, so the frame that starts the launch\n  // never waits on layout.\n  const snapshots = React.useRef(new Map<string, Docked>());\n  const dockedNow = React.useCallback((id: string): Docked | null => {\n    const stage = stageRef.current;\n    const tile = tiles.current.get(id);\n    if (!stage || !tile || !stage.offsetWidth || !tile.offsetWidth) return null;\n    const box = stage.getBoundingClientRect();\n    const rect = tile.getBoundingClientRect();\n    const k = box.width / stage.offsetWidth || 1; // a scaled ancestor, in or out\n    const W = stage.offsetWidth;\n    const H = stage.offsetHeight;\n    const scale = (Math.min(rect.width / k / W, rect.height / k / H) || 0.05) * DOCK.tuck;\n    const cx = (rect.left - box.left + rect.width / 2) / k;\n    const cy = (rect.top - box.top + rect.height / 2) / k;\n    return `translate(${cx - (W / 2) * scale}px, ${cy - (H / 2) * scale}px) scale(${scale})`;\n  }, []);\n  const docked = React.useCallback(\n    (id: string) => {\n      const taken = snapshots.current.get(id);\n      snapshots.current.delete(id);\n      return taken ?? dockedNow(id);\n    },\n    [dockedNow],\n  );\n\n  const select = React.useCallback(\n    (id: string, reason: \"pick\" | \"autoplay\") => {\n      if (id === activeRef.current) return;\n      const snapshot = dockedNow(id);\n      if (snapshot) snapshots.current.set(id, snapshot);\n      activeRef.current = id;\n      elapsed.current = 0;\n      setInner(id);\n      onValueChange?.(id, reason);\n    },\n    [onValueChange, dockedNow],\n  );\n\n  const choose = (id: string) => {\n    setStopped(true);\n    select(id, \"pick\");\n  };\n\n  // The stage scales every window's content from its design size to the width it gets.\n  React.useLayoutEffect(() => {\n    const stage = stageRef.current;\n    if (!stage) return;\n    const fit = () => {\n      stage.style.setProperty(\"--launch-dock-scale\", String(stage.offsetWidth / width));\n      stage.dataset.measured = \"\";\n    };\n    fit();\n    const observer = new ResizeObserver(fit);\n    observer.observe(stage);\n    return () => observer.disconnect();\n  }, [width]);\n\n  // Where the label and the dot sit over an icon, in the tray's px.\n  const centerOf = (id: string) => {\n    const tab = tabs.current.get(id);\n    return tab ? tab.offsetLeft + tab.offsetWidth / 2 : 0;\n  };\n\n  const placeLabel = React.useCallback((id: string, glide: boolean) => {\n    const w = (labelWidths.current.get(id) ?? 48) + DOCK.labelPad;\n    const x = centerOf(id) - w / 2;\n    if (glide) {\n      animate(labelX, x, TIMING.glide);\n      animate(labelW, w, TIMING.glide);\n    } else {\n      labelX.jump(x);\n      labelW.jump(w);\n    }\n  }, [labelX, labelW]);\n\n  const placeDot = React.useCallback(\n    (id: string | undefined, glide: boolean) => {\n      if (!id) return;\n      const w = running ? DOCK.bar : DOCK.dot;\n      const x = centerOf(id) - w / 2;\n      if (glide) {\n        animate(dotX, x, TIMING.glide);\n        animate(dotW, w, TIMING.glide);\n      } else {\n        dotX.jump(x);\n        dotW.jump(w);\n      }\n    },\n    [running, dotX, dotW],\n  );\n\n  // Measure every label once (and again when fonts land), so the label's width can glide.\n  React.useLayoutEffect(() => {\n    const box = measurer.current;\n    if (!box) return;\n    const read = () => {\n      box.querySelectorAll<HTMLElement>(\"[data-label]\").forEach((node) => {\n        labelWidths.current.set(node.dataset.label!, node.offsetWidth);\n      });\n    };\n    read();\n    let live = true;\n    document.fonts?.ready.then(() => {\n      if (live) read();\n    });\n    return () => {\n      live = false;\n    };\n  }, [items]);\n\n  React.useLayoutEffect(() => {\n    placeDot(active, placed.current.dot);\n    placed.current.dot = true;\n  }, [active, placeDot]);\n\n  React.useLayoutEffect(() => {\n    if (!chip?.on) return;\n    placeLabel(chip.id, placed.current.label);\n    placed.current.label = true;\n  }, [chip, placeLabel]);\n\n  // Icons resize with the container (cqi), so keep the label and dot over theirs.\n  React.useEffect(() => {\n    const tray = trayRef.current;\n    if (!tray) return;\n    const observer = new ResizeObserver(() => {\n      placeDot(activeRef.current, false);\n      if (chip) placeLabel(chip.id, false);\n    });\n    observer.observe(tray);\n    return () => observer.disconnect();\n  }, [chip, placeDot, placeLabel]);\n\n  React.useEffect(() => {\n    const update = () => setPageHidden(document.visibilityState === \"hidden\");\n    document.addEventListener(\"visibilitychange\", update);\n    return () => document.removeEventListener(\"visibilitychange\", update);\n  }, []);\n\n  // The countdown: a plain rAF clock that only runs while nothing pauses it, so the bar holds\n  // where it is and carries on from there.\n  React.useEffect(() => {\n    if (!running) {\n      fill.set(1);\n      return;\n    }\n    fill.set(clamp(elapsed.current / interval, 0, 1));\n    if (paused) return;\n    let frame = 0;\n    let last = performance.now();\n    const tick = (now: number) => {\n      elapsed.current += now - last;\n      last = now;\n      if (elapsed.current >= interval) {\n        const index = items.findIndex((item) => item.id === activeRef.current);\n        select(items[(index + 1) % items.length].id, \"autoplay\");\n      }\n      fill.set(clamp(elapsed.current / interval, 0, 1));\n      frame = requestAnimationFrame(tick);\n    };\n    frame = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(frame);\n  }, [running, paused, interval, items, select, fill]);\n\n  React.useEffect(\n    () => () => {\n      window.clearTimeout(leaveTimer.current);\n      window.clearTimeout(flashTimer.current);\n    },\n    [],\n  );\n\n  const showChip = (id: string) => {\n    window.clearTimeout(leaveTimer.current);\n    window.clearTimeout(flashTimer.current);\n    setChip({ id, on: true });\n  };\n  const hideChip = (delay: number) => {\n    window.clearTimeout(leaveTimer.current);\n    leaveTimer.current = window.setTimeout(() => {\n      setChip((current) => (current ? { ...current, on: false } : current));\n      setHover(null);\n    }, delay);\n  };\n\n  const handleTab = (type: TabEvent, id: string, event: React.SyntheticEvent<HTMLElement>) => {\n    const pointer = (event as React.PointerEvent<HTMLElement>).pointerType;\n    switch (type) {\n      case \"down\":\n        pointerType.current = pointer;\n        if ((event as React.PointerEvent<HTMLElement>).button === 0) setPressed(id);\n        break;\n      case \"up\":\n        setPressed(null);\n        break;\n      case \"enter\":\n        if (pointer !== \"mouse\") break;\n        setHover(id);\n        showChip(id);\n        break;\n      case \"leave\":\n        setPressed(null);\n        if (pointer === \"mouse\") hideChip(TIMING.leave);\n        break;\n      case \"focus\":\n        if (event.currentTarget.matches(\":focus-visible\")) showChip(id);\n        break;\n      case \"blur\":\n        hideChip(0);\n        break;\n      case \"click\":\n        if (pointerType.current !== \"mouse\" && pointerType.current !== \"keyboard\") {\n          showChip(id);\n          flashTimer.current = window.setTimeout(\n            () => setChip((current) => (current ? { ...current, on: false } : current)),\n            TIMING.touchLabel,\n          );\n        }\n        pointerType.current = \"mouse\";\n        choose(id);\n        break;\n    }\n  };\n  const tabHandler = React.useRef(handleTab);\n  React.useLayoutEffect(() => {\n    tabHandler.current = handleTab;\n  });\n  const onTabEvent = React.useCallback(\n    (type: TabEvent, id: string, event: React.SyntheticEvent<HTMLElement>) => tabHandler.current(type, id, event),\n    [],\n  );\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    const index = items.findIndex((item) => item.id === (event.target as HTMLElement).dataset.launchDockItem);\n    if (index < 0) return;\n    const last = items.length - 1;\n    const next =\n      event.key === \"ArrowRight\" || event.key === \"ArrowDown\"\n        ? (index + 1) % items.length\n        : event.key === \"ArrowLeft\" || event.key === \"ArrowUp\"\n          ? (index - 1 + items.length) % items.length\n          : event.key === \"Home\"\n            ? 0\n            : event.key === \"End\"\n              ? last\n              : -1;\n    if (next < 0) return;\n    event.preventDefault();\n    pointerType.current = \"keyboard\";\n    const id = items[next].id;\n    tabs.current.get(id)?.focus();\n    choose(id);\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      data-launch-dock=\"\"\n      className={cn(\"@container/launch-dock relative flex w-full flex-col items-center\", className)}\n      style={\n        {\n          \"--launch-dock-n\": Math.max(items.length, 1),\n          \"--launch-dock-icon\": \"clamp(1.75rem, min(calc((100cqi - 2.5rem) / var(--launch-dock-n) - 0.625rem), 9cqi), 3.5rem)\",\n          \"--launch-dock-corner\": \"calc(var(--launch-dock-icon) * var(--launch-dock-roundness, 0.225))\",\n          \"--launch-dock-gap\": \"clamp(0.375rem, calc(var(--launch-dock-icon) * 0.18), 0.625rem)\",\n          ...style,\n        } as React.CSSProperties\n      }\n      onPointerEnter={(event) => {\n        if (event.pointerType === \"mouse\") setHovering(true);\n        onPointerEnter?.(event);\n      }}\n      onPointerLeave={(event) => {\n        setHovering(false);\n        onPointerLeave?.(event);\n      }}\n      onFocus={(event) => {\n        setFocusWithin(true);\n        onFocus?.(event);\n      }}\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setFocusWithin(false);\n        onBlur?.(event);\n      }}\n      {...props}\n    >\n      <div\n        ref={stageRef}\n        data-launch-dock-stage=\"\"\n        className=\"group/launch-dock-stage relative z-0 w-full\"\n        style={{ maxWidth: width }}\n      >\n        {/* Holds the stage open at the window's size; the windows themselves are stacked over it. */}\n        <div aria-hidden className=\"pointer-events-none invisible flex flex-col\">\n          {frame === \"window\" ? <div className={cn(BAR, \"shrink-0\")} /> : null}\n          <div style={{ aspectRatio: `${width} / ${height}` }} />\n        </div>\n        {items.map((item) => (\n          <DockWindow\n            key={item.id}\n            item={item}\n            open={item.id === active}\n            frame={frame}\n            fade={fade}\n            reduced={reduced}\n            duration={Math.max(0.01, duration)}\n            docked={docked}\n            width={width}\n            height={height}\n            tabId={`${uid}-tab-${item.id}`}\n            panelId={`${uid}-panel-${item.id}`}\n            className={windowClassName}\n          />\n        ))}\n      </div>\n\n      <div\n        className={cn(\n          \"relative z-10 flex max-w-full justify-center px-2\",\n          fade ? \"-mt-[calc(var(--launch-dock-icon)*0.5+1.25rem)]\" : \"mt-4\",\n        )}\n      >\n        <div\n          ref={trayRef}\n          role=\"tablist\"\n          aria-label={dockLabel}\n          aria-orientation=\"horizontal\"\n          data-launch-dock-tray=\"\"\n          onKeyDown={onKeyDown}\n          className={cn(\n            \"relative flex items-end gap-[var(--launch-dock-gap)] rounded-[calc(var(--launch-dock-corner)+0.5rem)] bg-white/90 p-2 shadow-[0_12px_32px_-12px_rgb(0_0_0/0.28),0_2px_6px_-2px_rgb(0_0_0/0.08)] ring-1 ring-zinc-950/[0.07] dark:bg-zinc-900/90 dark:shadow-[0_12px_32px_-12px_rgb(0_0_0/0.7)] dark:ring-white/10\",\n            trayClassName,\n          )}\n        >\n          {items.map((item) => (\n            <DockTab\n              key={item.id}\n              item={item}\n              active={item.id === active}\n              lifted={!reduced && hover === item.id}\n              dipped={!reduced && pressed === item.id}\n              grow={hoverScale}\n              tabId={`${uid}-tab-${item.id}`}\n              panelId={`${uid}-panel-${item.id}`}\n              register={register}\n              onTabEvent={onTabEvent}\n            />\n          ))}\n\n          {/* The open app's dot, which counts down as a bar while autoplay runs. */}\n          <motion.span\n            aria-hidden\n            style={{ x: dotX, width: dotW }}\n            className=\"pointer-events-none absolute bottom-[3px] left-0 h-1 overflow-hidden rounded-full bg-zinc-950/20 dark:bg-white/25\"\n          >\n            <motion.span\n              style={{ scaleX: fill }}\n              className=\"absolute inset-0 origin-left rounded-full bg-zinc-950/60 dark:bg-white/75\"\n            />\n          </motion.span>\n\n          {/* One label for the whole dock: it glides from icon to icon instead of blinking. */}\n          <motion.div\n            aria-hidden\n            style={{ x: labelX, width: labelW }}\n            className=\"pointer-events-none absolute bottom-full left-0 mb-3 flex justify-center\"\n          >\n            <motion.div\n              initial={false}\n              animate={\n                chip?.on\n                  ? { opacity: 1, transform: \"translateY(0px) scale(1)\" }\n                  : { opacity: 0, transform: \"translateY(4px) scale(0.96)\" }\n              }\n              transition={{\n                ...(chip?.on ? TIMING.show : TIMING.hide),\n                transform: reduced ? { duration: 0 } : chip?.on ? TIMING.show : TIMING.hide,\n              }}\n              className=\"w-full origin-bottom rounded-lg bg-zinc-900 py-1 text-center text-xs font-medium whitespace-nowrap text-white shadow-[0_4px_12px_-4px_rgb(0_0_0/0.35)] ring-1 ring-white/10 dark:bg-zinc-100/95 dark:text-zinc-900 dark:ring-zinc-950/10\"\n            >\n              {items.find((item) => item.id === chip?.id)?.label ?? \"\"}\n            </motion.div>\n          </motion.div>\n\n          {/* Hidden copies of every label, measured once so the label's width can glide. */}\n          <div ref={measurer} aria-hidden className=\"pointer-events-none invisible absolute top-0 left-0 flex\">\n            {items.map((item) => (\n              <span key={item.id} data-label={item.id} className=\"text-xs font-medium whitespace-nowrap\">\n                {item.label}\n              </span>\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}