{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"morphing-tabs","type":"registry:block","title":"Morphing Tabs","description":"Reorderable tabs whose selected item grows into a white content surface, with the active shape gliding as tabs move and a shared morph between rooms.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/morphing-tabs.tsx","type":"registry:component","target":"@components/motion/morphing-tabs.tsx","content":"\"use client\";\n// beui.dev/components/blocks/morphing-tabs\n\nimport { X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  animate as animateValue,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n  type MotionValue,\n} from \"motion/react\";\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { EASE_OUT, SPRING_GLIDE, SPRING_PRESS } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type MorphingTabsItem = {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n  content: ReactNode;\n  disabled?: boolean;\n};\n\nexport type MorphingTabsClassNames = {\n  root?: string;\n  rail?: string;\n  tab?: string;\n  activeTab?: string;\n  icon?: string;\n  label?: string;\n  close?: string;\n  content?: string;\n};\n\nexport interface MorphingTabsProps {\n  items: MorphingTabsItem[];\n  value?: string | null;\n  defaultValue?: string | null;\n  onValueChange?: (id: string | null) => void;\n  /** Called once after a pointer drag or keyboard reorder completes. */\n  onOrderChange?: (ids: string[]) => void;\n  /** Enables the close affordance on every tab when provided. */\n  onClose?: (id: string) => void;\n  ariaLabel?: string;\n  className?: string;\n  classNames?: MorphingTabsClassNames;\n}\n\ntype DragSession = {\n  id: string;\n  pointerId: number;\n  originX: number;\n  startLeft: number;\n  startIndex: number;\n  targetIndex: number;\n  moved: boolean;\n  finishing: boolean;\n  startOrder: string[];\n  slotLefts: number[];\n};\n\ntype SpringTabProps = {\n  id: string;\n  targetLeft: number;\n  dragging: boolean;\n  dragLeft: MotionValue<number>;\n  surfaceLeft: MotionValue<number>;\n  reduce: boolean;\n  active: boolean;\n  anyDragging: boolean;\n  surfaceHost: HTMLDivElement | null;\n  surfaceWidth: number;\n  surfaceClassName?: string;\n  zIndex: number;\n  className: string;\n  children: ReactNode;\n  registerPosition: (id: string, position: MotionValue<number> | null) => void;\n  onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;\n  onPointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;\n  onPointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;\n  onPointerCancel: (event: ReactPointerEvent<HTMLDivElement>) => void;\n  onLostPointerCapture: (event: ReactPointerEvent<HTMLDivElement>) => void;\n};\n\nconst DRAG_THRESHOLD = 5;\nconst TAB_WIDTH = 176;\nconst TAB_HEIGHT = 56;\nconst TAB_TOP = 24;\nconst TAB_RADIUS = 24;\nconst RAIL_HEIGHT = 80;\nconst SURFACE_INSET = 16;\nconst LIQUID_JOIN = 24;\nconst PANEL_RADIUS = 28;\n\nfunction sameOrder(a: string[], b: string[]) {\n  return a.length === b.length && a.every((id, index) => id === b[index]);\n}\n\nfunction safeId(value: string) {\n  return value.replace(/[^a-zA-Z0-9_-]/g, \"-\");\n}\n\nfunction moveItem(order: string[], from: number, to: number) {\n  if (from === to) return order.slice();\n  const next = order.slice();\n  const [item] = next.splice(from, 1);\n  next.splice(to, 0, item);\n  return next;\n}\n\nfunction liquidTabPath(tabLeft: number, surfaceWidth: number) {\n  const panelLeft = SURFACE_INSET;\n  const panelRight = surfaceWidth - SURFACE_INSET;\n  const left = Math.max(\n    panelLeft,\n    Math.min(panelRight - TAB_WIDTH, tabLeft),\n  );\n  const right = left + TAB_WIDTH;\n  const top = RAIL_HEIGHT - TAB_HEIGHT;\n  const bottom = RAIL_HEIGHT;\n  const leftJoin = Math.max(panelLeft, left - LIQUID_JOIN);\n  const rightJoin = Math.min(panelRight, right + LIQUID_JOIN);\n  const leftDepth = Math.min(LIQUID_JOIN, left - leftJoin);\n  const rightDepth = Math.min(LIQUID_JOIN, rightJoin - right);\n  const leftControl = leftDepth * 0.55;\n  const rightControl = rightDepth * 0.55;\n  const leftPanelRadius = Math.min(PANEL_RADIUS, leftJoin - panelLeft);\n  const rightPanelRadius = Math.min(PANEL_RADIUS, panelRight - rightJoin);\n\n  return [\n    `M${panelLeft} ${bottom + PANEL_RADIUS}`,\n    `V${bottom + leftPanelRadius}`,\n    `Q${panelLeft} ${bottom} ${panelLeft + leftPanelRadius} ${bottom}`,\n    `H${leftJoin}`,\n    `C${leftJoin + leftControl} ${bottom} ${left} ${bottom - leftDepth + leftControl} ${left} ${bottom - leftDepth}`,\n    `V${top + TAB_RADIUS}`,\n    `Q${left} ${top} ${left + TAB_RADIUS} ${top}`,\n    `H${right - TAB_RADIUS}`,\n    `Q${right} ${top} ${right} ${top + TAB_RADIUS}`,\n    `V${bottom - rightDepth}`,\n    `C${right} ${bottom - rightDepth + rightControl} ${rightJoin - rightControl} ${bottom} ${rightJoin} ${bottom}`,\n    `H${panelRight - rightPanelRadius}`,\n    `Q${panelRight} ${bottom} ${panelRight} ${bottom + rightPanelRadius}`,\n    `V${bottom + PANEL_RADIUS}`,\n    \"Z\",\n  ].join(\" \");\n}\n\nfunction SpringTab({\n  id,\n  targetLeft,\n  dragging,\n  dragLeft,\n  surfaceLeft,\n  reduce,\n  active,\n  anyDragging,\n  surfaceHost,\n  surfaceWidth,\n  surfaceClassName,\n  zIndex,\n  className,\n  children,\n  registerPosition,\n  onPointerDown,\n  onPointerMove,\n  onPointerUp,\n  onPointerCancel,\n  onLostPointerCapture,\n}: SpringTabProps) {\n  const target = useMotionValue(targetLeft);\n  const position = useSpring(target, SPRING_GLIDE);\n  const settledTransform = useTransform(\n    reduce ? target : position,\n    (left) => `translate3d(${left}px, 0, 0)`,\n  );\n  const draggedTransform = useTransform(\n    dragLeft,\n    (left) => `translate3d(${left}px, 0, 0)`,\n  );\n\n  useLayoutEffect(() => {\n    target.set(targetLeft);\n    if (reduce) position.jump(targetLeft);\n  }, [position, reduce, target, targetLeft]);\n\n  useLayoutEffect(() => {\n    registerPosition(id, position);\n    return () => registerPosition(id, null);\n  }, [id, position, registerPosition]);\n\n  const liquidDriver = anyDragging\n    ? dragging\n      ? dragLeft\n      : position\n    : surfaceLeft;\n\n  return (\n    <>\n      {active && surfaceHost && surfaceWidth > SURFACE_INSET * 2\n        ? createPortal(\n            <svg\n              aria-hidden=\"true\"\n              focusable=\"false\"\n              viewBox={`0 0 ${surfaceWidth} ${RAIL_HEIGHT + PANEL_RADIUS}`}\n              preserveAspectRatio=\"none\"\n              className={cn(\n                \"pointer-events-none absolute inset-x-0 top-0 h-[108px] w-full text-[#fafaf8]\",\n                dragging ? \"z-20\" : \"z-0\",\n                surfaceClassName,\n              )}\n            >\n              <LiquidSurfacePath\n                key={\n                  anyDragging\n                    ? dragging\n                      ? \"dragged\"\n                      : \"displaced\"\n                    : \"idle\"\n                }\n                left={liquidDriver}\n                surfaceWidth={surfaceWidth}\n              />\n            </svg>,\n            surfaceHost,\n          )\n        : null}\n      <motion.div\n        style={{\n          zIndex,\n          transform: dragging ? draggedTransform : settledTransform,\n        }}\n        className={className}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={onPointerUp}\n        onPointerCancel={onPointerCancel}\n        onLostPointerCapture={onLostPointerCapture}\n      >\n        {children}\n      </motion.div>\n    </>\n  );\n}\n\nfunction LiquidSurfacePath({\n  left,\n  surfaceWidth,\n}: {\n  left: MotionValue<number>;\n  surfaceWidth: number;\n}) {\n  const path = useTransform(left, (value) =>\n    liquidTabPath(value, surfaceWidth),\n  );\n  return <motion.path d={path} fill=\"currentColor\" />;\n}\n\nexport function MorphingTabs({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  onOrderChange,\n  onClose,\n  ariaLabel = \"Tabs\",\n  className,\n  classNames,\n}: MorphingTabsProps) {\n  const reduce = Boolean(useReducedMotion());\n  const uid = useId();\n  const itemIds = useMemo(() => items.map((item) => item.id), [items]);\n  const itemMap = useMemo(\n    () => new Map(items.map((item) => [item.id, item])),\n    [items],\n  );\n  const [order, setOrder] = useState(itemIds);\n  const orderRef = useRef(order);\n  orderRef.current = order;\n\n  const [internalValue, setInternalValue] = useState<string | null>(\n    defaultValue ?? itemIds[0] ?? null,\n  );\n  const controlled = value !== undefined;\n  const currentValue = controlled ? (value ?? null) : internalValue;\n\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const railRef = useRef<HTMLDivElement | null>(null);\n  const tabButtonRefs = useRef<Record<string, HTMLButtonElement | null>>({});\n  const tabPositionRefs = useRef<Record<string, MotionValue<number> | null>>(\n    {},\n  );\n  const dragRef = useRef<DragSession | null>(null);\n  const dragAnimationRef = useRef<ReturnType<typeof animateValue> | null>(null);\n  const surfaceAnimationRef = useRef<ReturnType<typeof animateValue> | null>(\n    null,\n  );\n  const [surfaceWidth, setSurfaceWidth] = useState(0);\n  const [tabGap, setTabGap] = useState(12);\n  const [draggingId, setDraggingId] = useState<string | null>(null);\n  const [dragTargetIndex, setDragTargetIndex] = useState(-1);\n  const dragLeft = useMotionValue(SURFACE_INSET);\n  const surfaceLeft = useMotionValue(SURFACE_INSET);\n\n  useEffect(() => {\n    setOrder((current) => {\n      const available = new Set(itemIds);\n      const retained = current.filter((id) => available.has(id));\n      const retainedSet = new Set(retained);\n      const added = itemIds.filter((id) => !retainedSet.has(id));\n      const next = [...retained, ...added];\n      return sameOrder(current, next) ? current : next;\n    });\n  }, [itemIds]);\n\n  const orderedItems = useMemo(\n    () =>\n      order.flatMap((id) => {\n        const item = itemMap.get(id);\n        return item ? [item] : [];\n      }),\n    [itemMap, order],\n  );\n\n  const firstEnabledItem =\n    orderedItems.find((item) => !item.disabled) ?? orderedItems[0] ?? null;\n  const activeItem =\n    currentValue && itemMap.has(currentValue)\n      ? itemMap.get(currentValue) ?? null\n      : firstEnabledItem;\n  const activeId = activeItem?.id ?? null;\n\n  const slotLefts = useMemo(\n    () =>\n      order.map(\n        (_, index) => SURFACE_INSET + index * (TAB_WIDTH + tabGap),\n      ),\n    [order, tabGap],\n  );\n\n  const dragStartIndex = draggingId ? order.indexOf(draggingId) : -1;\n\n  const visualIndexFor = useCallback(\n    (index: number) => {\n      if (dragStartIndex < 0 || dragTargetIndex < 0) return index;\n      if (index === dragStartIndex) return dragTargetIndex;\n\n      if (\n        dragTargetIndex > dragStartIndex &&\n        index > dragStartIndex &&\n        index <= dragTargetIndex\n      ) {\n        return index - 1;\n      }\n      if (\n        dragTargetIndex < dragStartIndex &&\n        index >= dragTargetIndex &&\n        index < dragStartIndex\n      ) {\n        return index + 1;\n      }\n      return index;\n    },\n    [dragStartIndex, dragTargetIndex],\n  );\n\n  useLayoutEffect(() => {\n    const root = rootRef.current;\n    const rail = railRef.current;\n    if (!root || !rail) return;\n\n    const measure = () => {\n      setSurfaceWidth(root.clientWidth);\n      const nextGap = Number.parseFloat(getComputedStyle(rail).columnGap);\n      if (Number.isFinite(nextGap)) setTabGap(nextGap);\n    };\n\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(root);\n    return () => observer.disconnect();\n  }, []);\n\n  const setActive = useCallback(\n    (id: string | null) => {\n      if (id && itemMap.get(id)?.disabled) return;\n      if (!controlled) setInternalValue(id);\n      onValueChange?.(id);\n    },\n    [controlled, itemMap, onValueChange],\n  );\n\n  useEffect(() => {\n    if (currentValue && itemMap.has(currentValue)) return;\n    if (firstEnabledItem && firstEnabledItem.id !== currentValue) {\n      setActive(firstEnabledItem.id);\n    }\n  }, [currentValue, firstEnabledItem, itemMap, setActive]);\n\n  const activeOrderIndex = activeId ? order.indexOf(activeId) : -1;\n  const activeVisualIndex =\n    activeOrderIndex < 0 ? -1 : visualIndexFor(activeOrderIndex);\n\n  useLayoutEffect(() => {\n    if (\n      !activeId ||\n      activeVisualIndex < 0 ||\n      activeId === draggingId ||\n      !slotLefts[activeVisualIndex]\n    ) {\n      return;\n    }\n\n    surfaceAnimationRef.current?.stop();\n\n    if (draggingId) return;\n\n    surfaceAnimationRef.current = animateValue(\n      surfaceLeft,\n      slotLefts[activeVisualIndex],\n      reduce ? { duration: 0 } : SPRING_GLIDE,\n    );\n  }, [\n    activeId,\n    activeVisualIndex,\n    draggingId,\n    reduce,\n    slotLefts,\n    surfaceLeft,\n  ]);\n\n  const commitOrder = useCallback(\n    (next: string[], notify: boolean) => {\n      orderRef.current = next;\n      setOrder((current) => (sameOrder(current, next) ? current : next));\n      if (notify) onOrderChange?.(next);\n    },\n    [onOrderChange],\n  );\n\n  const registerPosition = useCallback(\n    (id: string, position: MotionValue<number> | null) => {\n      tabPositionRefs.current[id] = position;\n    },\n    [],\n  );\n\n  const startDrag = useCallback(\n    (id: string, event: ReactPointerEvent<HTMLDivElement>) => {\n      if (\n        event.button !== 0 ||\n        itemMap.get(id)?.disabled ||\n        dragRef.current\n      ) {\n        return;\n      }\n\n      const startIndex = orderRef.current.indexOf(id);\n      if (startIndex < 0) return;\n      const capturedSlots = orderRef.current.map(\n        (_, index) => SURFACE_INSET + index * (TAB_WIDTH + tabGap),\n      );\n      const startLeft = capturedSlots[startIndex];\n\n      dragAnimationRef.current?.stop();\n      dragAnimationRef.current = null;\n      dragLeft.set(startLeft);\n      dragRef.current = {\n        id,\n        pointerId: event.pointerId,\n        originX: event.clientX,\n        startLeft,\n        startIndex,\n        targetIndex: startIndex,\n        moved: false,\n        finishing: false,\n        startOrder: orderRef.current.slice(),\n        slotLefts: capturedSlots,\n      };\n    },\n    [dragLeft, itemMap, tabGap],\n  );\n\n  const moveDrag = useCallback(\n    (event: ReactPointerEvent<HTMLDivElement>) => {\n      const drag = dragRef.current;\n      if (!drag || drag.finishing || drag.pointerId !== event.pointerId) return;\n\n      const delta = event.clientX - drag.originX;\n      if (!drag.moved && Math.abs(delta) < DRAG_THRESHOLD) return;\n      event.preventDefault();\n\n      if (!drag.moved) {\n        drag.moved = true;\n        event.currentTarget.setPointerCapture(event.pointerId);\n        if (drag.id === activeId) {\n          surfaceAnimationRef.current?.stop();\n          surfaceLeft.set(drag.startLeft);\n        }\n        setDraggingId(drag.id);\n        setDragTargetIndex(drag.startIndex);\n      }\n\n      const minLeft = drag.slotLefts[0];\n      const maxLeft = drag.slotLefts[drag.slotLefts.length - 1];\n      const visualLeft = Math.max(\n        minLeft,\n        Math.min(maxLeft, drag.startLeft + delta),\n      );\n      let targetIndex = drag.startIndex;\n\n      if (visualLeft >= drag.startLeft) {\n        for (\n          let index = drag.startIndex + 1;\n          index < drag.slotLefts.length;\n          index += 1\n        ) {\n          if (visualLeft + TAB_WIDTH / 2 >= drag.slotLefts[index]) {\n            targetIndex = index;\n          }\n        }\n      } else {\n        for (let index = drag.startIndex - 1; index >= 0; index -= 1) {\n          if (visualLeft <= drag.slotLefts[index] + TAB_WIDTH / 2) {\n            targetIndex = index;\n          }\n        }\n      }\n\n      dragLeft.set(visualLeft);\n      if (targetIndex !== drag.targetIndex) {\n        drag.targetIndex = targetIndex;\n        setDragTargetIndex(targetIndex);\n      }\n    },\n    [activeId, dragLeft, surfaceLeft],\n  );\n\n  const finishDrag = useCallback(\n    (pointerId: number) => {\n      const drag = dragRef.current;\n      if (!drag || drag.pointerId !== pointerId || drag.finishing) return;\n\n      if (!drag.moved) {\n        dragRef.current = null;\n        return;\n      }\n\n      drag.finishing = true;\n      const targetLeft = drag.slotLefts[drag.targetIndex];\n      const controls = animateValue(\n        dragLeft,\n        targetLeft,\n        reduce ? { duration: 0 } : SPRING_GLIDE,\n      );\n      dragAnimationRef.current = controls;\n\n      controls.then(async () => {\n        if (dragAnimationRef.current !== controls) return;\n        const next = moveItem(\n          drag.startOrder,\n          drag.startIndex,\n          drag.targetIndex,\n        );\n\n        if (!reduce) {\n          await new Promise<void>((resolve) => {\n            const startedAt = performance.now();\n            const check = () => {\n              const settled = next.every((id, index) => {\n                if (id === drag.id) return true;\n                const position = tabPositionRefs.current[id];\n                if (!position) return true;\n                return (\n                  Math.abs(position.get() - drag.slotLefts[index]) < 0.5 &&\n                  Math.abs(position.getVelocity()) < 10\n                );\n              });\n\n              if (settled || performance.now() - startedAt > 500) {\n                resolve();\n                return;\n              }\n              requestAnimationFrame(check);\n            };\n            check();\n          });\n        }\n\n        if (dragAnimationRef.current !== controls) return;\n        if (drag.id === activeId) {\n          surfaceLeft.set(targetLeft);\n        } else if (activeId) {\n          const activePosition = tabPositionRefs.current[activeId];\n          if (activePosition) surfaceLeft.set(activePosition.get());\n        }\n        tabPositionRefs.current[drag.id]?.jump(targetLeft);\n        dragAnimationRef.current = null;\n        dragRef.current = null;\n        commitOrder(next, !sameOrder(drag.startOrder, next));\n        setDraggingId(null);\n        setDragTargetIndex(-1);\n      });\n    },\n    [activeId, commitOrder, dragLeft, reduce, surfaceLeft],\n  );\n\n  useEffect(() => {\n    const finishFromWindow = (event: PointerEvent) => {\n      finishDrag(event.pointerId);\n    };\n    window.addEventListener(\"pointerup\", finishFromWindow, true);\n    window.addEventListener(\"pointercancel\", finishFromWindow, true);\n    return () => {\n      window.removeEventListener(\"pointerup\", finishFromWindow, true);\n      window.removeEventListener(\"pointercancel\", finishFromWindow, true);\n    };\n  }, [finishDrag]);\n\n  const moveBy = useCallback(\n    (id: string, direction: -1 | 1) => {\n      const current = orderRef.current;\n      const index = current.indexOf(id);\n      const nextIndex = index + direction;\n      if (\n        index < 0 ||\n        nextIndex < 0 ||\n        nextIndex >= current.length ||\n        itemMap.get(id)?.disabled\n      ) {\n        return;\n      }\n      commitOrder(moveItem(current, index, nextIndex), true);\n    },\n    [commitOrder, itemMap],\n  );\n\n  const handleTabKeyDown = useCallback(\n    (id: string, event: React.KeyboardEvent<HTMLButtonElement>) => {\n      const index = orderRef.current.indexOf(id);\n      if (index < 0) return;\n\n      if (\n        event.altKey &&\n        (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\")\n      ) {\n        event.preventDefault();\n        moveBy(id, event.key === \"ArrowLeft\" ? -1 : 1);\n        return;\n      }\n      if (event.key !== \"ArrowLeft\" && event.key !== \"ArrowRight\") return;\n\n      event.preventDefault();\n      const direction = event.key === \"ArrowLeft\" ? -1 : 1;\n      const nextIndex =\n        (index + direction + orderRef.current.length) % orderRef.current.length;\n      const nextId = orderRef.current[nextIndex];\n      setActive(nextId);\n      requestAnimationFrame(() => tabButtonRefs.current[nextId]?.focus());\n    },\n    [moveBy, setActive],\n  );\n\n  if (!orderedItems.length) return null;\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\n        \"relative isolate min-w-0 overflow-hidden rounded-[2rem] bg-[#292929] text-white\",\n        classNames?.root,\n        className,\n      )}\n    >\n      <div className=\"relative h-20\">\n        <div\n          ref={railRef}\n          role=\"tablist\"\n          aria-label={ariaLabel}\n          aria-orientation=\"horizontal\"\n          className={cn(\n            \"relative z-30 flex h-full gap-3 md:gap-4\",\n            classNames?.rail,\n          )}\n        >\n          {orderedItems.map((item, index) => {\n            const isActive = item.id === activeId;\n            const isDragging = item.id === draggingId;\n            const visualIndex = visualIndexFor(index);\n            const targetLeft = slotLefts[visualIndex] ?? SURFACE_INSET;\n            const tabId = `${uid}-tab-${safeId(item.id)}`;\n\n            return (\n              <SpringTab\n                key={item.id}\n                id={item.id}\n                targetLeft={targetLeft}\n                dragging={isDragging}\n                dragLeft={dragLeft}\n                surfaceLeft={surfaceLeft}\n                reduce={reduce}\n                active={isActive}\n                anyDragging={Boolean(draggingId)}\n                surfaceHost={rootRef.current}\n                surfaceWidth={surfaceWidth}\n                surfaceClassName={classNames?.activeTab}\n                zIndex={isDragging ? 30 : isActive ? 20 : 1}\n                className={cn(\n                  \"group absolute left-0 top-0 flex select-none touch-pan-y items-stretch\",\n                  item.disabled && \"cursor-not-allowed\",\n                  isDragging ? \"cursor-grabbing\" : \"cursor-grab\",\n                )}\n                registerPosition={registerPosition}\n                onPointerDown={(event) => startDrag(item.id, event)}\n                onPointerMove={moveDrag}\n                onPointerUp={(event) => finishDrag(event.pointerId)}\n                onPointerCancel={(event) => finishDrag(event.pointerId)}\n                onLostPointerCapture={(event) => finishDrag(event.pointerId)}\n              >\n                <div\n                  style={{\n                    width: TAB_WIDTH,\n                    height: TAB_HEIGHT,\n                    marginTop: TAB_TOP,\n                  }}\n                  className=\"relative flex items-stretch\"\n                >\n                  {!isActive ? (\n                    <span\n                      aria-hidden\n                      className={cn(\n                        \"absolute inset-x-0 bottom-2 top-0 rounded-[1.25rem] transition-colors duration-200\",\n                        isDragging\n                          ? \"bg-[#3a3a3a]\"\n                          : \"bg-transparent group-hover:bg-white/[0.06]\",\n                      )}\n                    />\n                  ) : null}\n\n                  <button\n                    ref={(node) => {\n                      tabButtonRefs.current[item.id] = node;\n                    }}\n                    id={tabId}\n                    type=\"button\"\n                    role=\"tab\"\n                    aria-selected={isActive}\n                    aria-controls={`${uid}-panel`}\n                    aria-disabled={item.disabled || undefined}\n                    tabIndex={isActive ? 0 : -1}\n                    disabled={item.disabled}\n                    onClick={() => {\n                      const drag = dragRef.current;\n                      if (drag?.id === item.id && drag.moved) return;\n                      setActive(item.id);\n                    }}\n                    onKeyDown={(event) => handleTabKeyDown(item.id, event)}\n                    className={cn(\n                      \"group relative z-10 flex h-full w-full min-w-0 items-center gap-2 overflow-hidden rounded-t-[1.5rem] px-3 text-left outline-none transition-colors\",\n                      isActive\n                        ? \"text-[#181818]\"\n                        : \"pb-2 text-white/70 hover:text-white\",\n                      classNames?.tab,\n                    )}\n                  >\n                    <span\n                      aria-hidden\n                      className={cn(\n                        \"pointer-events-none absolute inset-x-1 top-1 opacity-0 transition-opacity group-focus-visible:opacity-100\",\n                        isActive\n                          ? \"bottom-0 rounded-t-[1.25rem] border-x-2 border-t-2 border-black/20\"\n                          : \"bottom-2 rounded-[1rem] border-2 border-white/60\",\n                      )}\n                    />\n                    {item.icon ? (\n                      <span\n                        aria-hidden\n                        className={cn(\n                          \"grid size-8 shrink-0 place-items-center\",\n                          classNames?.icon,\n                        )}\n                      >\n                        {item.icon}\n                      </span>\n                    ) : null}\n                    <span\n                      className={cn(\n                        \"min-w-0 truncate whitespace-nowrap text-base font-medium leading-none tracking-[-0.025em]\",\n                        classNames?.label,\n                      )}\n                    >\n                      {item.label}\n                    </span>\n                  </button>\n\n                  {onClose ? (\n                    <button\n                      type=\"button\"\n                      aria-label={`Close ${item.label}`}\n                      onPointerDown={(event) => event.stopPropagation()}\n                      onClick={(event) => {\n                        event.stopPropagation();\n                        onClose(item.id);\n                      }}\n                      className={cn(\n                        \"absolute right-2 top-1/2 z-20 grid size-6 -translate-y-1/2 place-items-center rounded-full text-[#9aa0a8] transition-colors hover:bg-black/[0.06] hover:text-[#4b5563] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current\",\n                        !isActive &&\n                          \"top-[calc(50%-4px)] text-white/45 hover:bg-white/[0.08] hover:text-white/80\",\n                        classNames?.close,\n                      )}\n                    >\n                      <X aria-hidden className=\"size-3.5 stroke-[1.5]\" />\n                    </button>\n                  ) : null}\n                </div>\n              </SpringTab>\n            );\n          })}\n        </div>\n      </div>\n\n      <div\n        id={`${uid}-panel`}\n        role=\"tabpanel\"\n        aria-labelledby={`${uid}-tab-${safeId(activeId ?? \"empty\")}`}\n        className={cn(\n          \"relative z-20 mx-4 min-h-64 overflow-hidden rounded-[1.75rem] bg-[#fafaf8] text-[#181818]\",\n          classNames?.content,\n        )}\n      >\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {activeItem ? (\n            <motion.div\n              key={activeItem.id}\n              initial={\n                reduce\n                  ? { opacity: 0 }\n                  : { opacity: 0, y: 8, filter: \"blur(6px)\" }\n              }\n              animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n              exit={\n                reduce\n                  ? {\n                      opacity: 0,\n                      transition: { duration: 0.08, ease: EASE_OUT },\n                    }\n                  : {\n                      opacity: 0,\n                      y: -5,\n                      filter: \"blur(5px)\",\n                      transition: { duration: 0.12, ease: EASE_OUT },\n                    }\n              }\n              transition={\n                reduce\n                  ? { duration: 0.12, ease: EASE_OUT }\n                  : SPRING_PRESS\n              }\n              className=\"min-h-64\"\n            >\n              {activeItem.content}\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}