{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"adaptive-stepper","type":"registry:component","title":"Adaptive Stepper","description":"Composable numeric stepper whose fixed footprint adapts at its minimum and maximum while the value rolls between steps.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/adaptive-stepper.tsx","type":"registry:component","target":"@components/motion/adaptive-stepper.tsx","content":"\"use client\";\n// beui.dev/components/motion/adaptive-stepper\n\nimport { Minus, Plus } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  createContext,\n  type MouseEvent,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  EASE_OUT,\n  SPRING_PRESS,\n} from \"@/lib/ease\";\nimport {\n  Liquid,\n  LiquidItem,\n  type LiquidTransition,\n} from \"@/components/motion/liquid\";\nimport { cn } from \"@/lib/utils\";\n\n// The deliberately elastic separation curve from the liquid email reference.\nconst STEPPER_LIQUID_TRANSITION = {\n  duration: 600,\n  ease: [0.22, 1.3, 0.71, 1],\n} as const satisfies LiquidTransition;\n\ntype StepDirection = -1 | 0 | 1;\n\ntype AdaptiveStepperContextValue = {\n  value: number;\n  valueText: string;\n  direction: StepDirection;\n  atMin: boolean;\n  atMax: boolean;\n  disabled: boolean;\n  reduce: boolean;\n  decrement: (restoreFocus: boolean) => void;\n  increment: (restoreFocus: boolean) => void;\n  decrementRef: React.MutableRefObject<HTMLButtonElement | null>;\n  incrementRef: React.MutableRefObject<HTMLButtonElement | null>;\n};\n\nconst AdaptiveStepperContext = createContext<AdaptiveStepperContextValue | null>(\n  null,\n);\n\nfunction useAdaptiveStepperContext(component: string) {\n  const context = useContext(AdaptiveStepperContext);\n  if (!context) {\n    throw new Error(`${component} must be used within <AdaptiveStepper>`);\n  }\n  return context;\n}\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value));\n}\n\nfunction cleanNumber(value: number) {\n  return Number(value.toFixed(10));\n}\n\nfunction nextStep(\n  value: number,\n  direction: -1 | 1,\n  min: number,\n  max: number,\n  step: number,\n) {\n  if (direction === 1) {\n    const nextIndex = Math.floor((value - min) / step + 1e-10) + 1;\n    return cleanNumber(Math.min(max, min + nextIndex * step));\n  }\n\n  const previousIndex = Math.ceil((value - min) / step - 1e-10) - 1;\n  return cleanNumber(Math.max(min, min + previousIndex * step));\n}\n\nfunction mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref && typeof ref === \"object\") {\n        (ref as React.MutableRefObject<T | null>).current = node;\n      }\n    }\n  };\n}\n\nexport interface AdaptiveStepperProps {\n  children: ReactNode;\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (value: number) => void;\n  min?: number;\n  max?: number;\n  step?: number;\n  disabled?: boolean;\n  name?: string;\n  formatValueText?: (value: number) => string;\n  className?: string;\n  \"aria-label\"?: string;\n}\n\nexport function AdaptiveStepper({\n  children,\n  value: controlledValue,\n  defaultValue = 0,\n  onValueChange,\n  min = 0,\n  max = 10,\n  step = 1,\n  disabled = false,\n  name,\n  formatValueText,\n  className,\n  \"aria-label\": ariaLabel = \"Quantity\",\n}: AdaptiveStepperProps) {\n  const reduce = useReducedMotion() ?? false;\n  const labelId = useId();\n  const decrementRef = useRef<HTMLButtonElement>(null);\n  const incrementRef = useRef<HTMLButtonElement>(null);\n  const lower = Number.isFinite(min) ? min : 0;\n  const suppliedMax = Number.isFinite(max) ? max : lower;\n  const upper = suppliedMax > lower ? suppliedMax : lower;\n  const stride = Number.isFinite(step) && step > 0 ? step : 1;\n  const [internalValue, setInternalValue] = useState(() =>\n    clamp(Number.isFinite(defaultValue) ? defaultValue : lower, lower, upper),\n  );\n  const controlled = controlledValue !== undefined;\n  const suppliedValue = controlled ? controlledValue : internalValue;\n  const currentValue = clamp(\n    Number.isFinite(suppliedValue) ? suppliedValue : lower,\n    lower,\n    upper,\n  );\n  const previousValueRef = useRef(currentValue);\n  const currentValueRef = useRef(currentValue);\n  const direction: StepDirection =\n    currentValue === previousValueRef.current\n      ? 0\n      : currentValue > previousValueRef.current\n        ? 1\n        : -1;\n\n  useLayoutEffect(() => {\n    previousValueRef.current = currentValue;\n    currentValueRef.current = currentValue;\n  }, [currentValue]);\n\n  const commit = useCallback(\n    (nextValue: number, restoreFocus: boolean) => {\n      const next = clamp(cleanNumber(nextValue), lower, upper);\n      if (next === currentValue) return;\n      if (!controlled) setInternalValue(next);\n      onValueChange?.(next);\n\n      if (!restoreFocus) return;\n      requestAnimationFrame(() => {\n        if (next === upper && currentValueRef.current === upper) {\n          decrementRef.current?.focus();\n        } else if (next === lower && currentValueRef.current === lower) {\n          incrementRef.current?.focus();\n        }\n      });\n    },\n    [controlled, currentValue, lower, onValueChange, upper],\n  );\n\n  const decrement = useCallback(\n    (restoreFocus: boolean) => {\n      if (disabled || currentValue <= lower) return;\n      commit(nextStep(currentValue, -1, lower, upper, stride), restoreFocus);\n    },\n    [commit, currentValue, disabled, lower, stride, upper],\n  );\n\n  const increment = useCallback(\n    (restoreFocus: boolean) => {\n      if (disabled || currentValue >= upper) return;\n      commit(nextStep(currentValue, 1, lower, upper, stride), restoreFocus);\n    },\n    [commit, currentValue, disabled, lower, stride, upper],\n  );\n\n  const valueText = formatValueText?.(currentValue) ?? String(currentValue);\n  const context = useMemo<AdaptiveStepperContextValue>(\n    () => ({\n      value: currentValue,\n      valueText,\n      direction,\n      atMin: currentValue <= lower,\n      atMax: currentValue >= upper,\n      disabled,\n      reduce,\n      decrement,\n      increment,\n      decrementRef,\n      incrementRef,\n    }),\n    [\n      currentValue,\n      decrement,\n      direction,\n      disabled,\n      increment,\n      lower,\n      reduce,\n      upper,\n      valueText,\n    ],\n  );\n\n  return (\n    <AdaptiveStepperContext.Provider value={context}>\n      <fieldset\n        disabled={disabled}\n        className={cn(\n          \"relative isolate m-0 inline-block h-12 w-[13.5rem] border-0 p-0\",\n          className,\n        )}\n      >\n        <legend id={labelId} className=\"sr-only\">\n          {ariaLabel}. Current value: {valueText}\n        </legend>\n        <Liquid\n          blur={8}\n          contrast={22}\n          fill=\"var(--background)\"\n          className=\"size-full\"\n        >\n          {children}\n        </Liquid>\n        {name ? <input type=\"hidden\" name={name} value={currentValue} /> : null}\n      </fieldset>\n    </AdaptiveStepperContext.Provider>\n  );\n}\n\nexport interface AdaptiveStepperActionProps\n  extends Omit<HTMLMotionProps<\"button\">, \"children\" | \"onClick\"> {\n  children?: ReactNode;\n  onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n  ref?: Ref<HTMLButtonElement>;\n}\n\nfunction StepperAction({\n  direction,\n  children,\n  className,\n  onClick,\n  ref,\n  style,\n  tabIndex,\n  \"aria-label\": ariaLabel,\n  ...props\n}: AdaptiveStepperActionProps & { direction: -1 | 1 }) {\n  const context = useAdaptiveStepperContext(\"AdaptiveStepper action\");\n  const hidden = direction === -1 ? context.atMin : context.atMax;\n  const actionRef =\n    direction === -1 ? context.decrementRef : context.incrementRef;\n  const label =\n    ariaLabel ?? (direction === -1 ? \"Decrease value\" : \"Increase value\");\n  const action = direction === -1 ? context.decrement : context.increment;\n  const x =\n    direction === -1\n      ? hidden\n        ? 32\n        : 0\n      : hidden\n        ? 136\n        : 168;\n\n  return (\n    <LiquidItem\n      x={x}\n      y={0}\n      width={48}\n      height={48}\n      radius={24}\n      transition={STEPPER_LIQUID_TRANSITION}\n    >\n      <motion.button\n        {...props}\n        ref={mergeRefs(ref, actionRef)}\n        type=\"button\"\n        aria-label={label}\n        aria-hidden={hidden || undefined}\n        tabIndex={hidden ? -1 : tabIndex}\n        disabled={context.disabled || hidden}\n        whileTap={\n          context.reduce || context.disabled || hidden\n            ? undefined\n            : { scale: 0.94 }\n        }\n        transition={context.reduce ? { duration: 0 } : SPRING_PRESS}\n        style={style}\n        className={cn(\n          \"grid size-full place-items-center rounded-full border border-transparent bg-transparent bg-clip-padding text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none\",\n          hidden && \"hover:bg-transparent\",\n          context.disabled && \"opacity-50\",\n          className,\n        )}\n        onClick={(event) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) action(event.detail === 0);\n        }}\n      >\n        <motion.span\n          aria-hidden=\"true\"\n          initial={{\n            opacity: hidden ? 0 : 1,\n            filter: hidden ? \"blur(2px)\" : \"blur(0px)\",\n          }}\n          animate={{\n            opacity: hidden ? 0 : 1,\n            filter: hidden ? \"blur(2px)\" : \"blur(0px)\",\n          }}\n          transition={{ duration: context.reduce ? 0 : 0.15, ease: EASE_OUT }}\n        >\n          {children ??\n            (direction === -1 ? (\n              <Minus className=\"size-5\" strokeWidth={2.5} />\n            ) : (\n              <Plus className=\"size-5\" strokeWidth={2.5} />\n            ))}\n        </motion.span>\n      </motion.button>\n    </LiquidItem>\n  );\n}\n\nexport function AdaptiveStepperDecrement(props: AdaptiveStepperActionProps) {\n  return <StepperAction {...props} direction={-1} />;\n}\n\nexport interface AdaptiveStepperValueProps\n  extends Omit<HTMLMotionProps<\"output\">, \"children\"> {\n  children?: ReactNode | ((value: number) => ReactNode);\n}\n\nexport function AdaptiveStepperValue({\n  children,\n  className,\n  style,\n  ...props\n}: AdaptiveStepperValueProps) {\n  const context = useAdaptiveStepperContext(\"AdaptiveStepperValue\");\n  const geometry =\n    context.atMin && context.atMax\n      ? { x: 0, width: 216 }\n      : context.atMin\n        ? { x: 0, width: 152 }\n        : context.atMax\n          ? { x: 64, width: 152 }\n          : { x: 64, width: 88 };\n  const displayValue =\n    typeof children === \"function\" ? children(context.value) : children;\n  const renderedValue = displayValue ?? context.value;\n  const canRoll =\n    typeof renderedValue === \"number\" || typeof renderedValue === \"string\";\n  const distance = context.reduce || !canRoll ? 0 : context.direction * 32;\n  const enterFrom = `translateY(${distance}%)`;\n  const exitTo = `translateY(${-distance}%)`;\n\n  return (\n    <LiquidItem\n      x={geometry.x}\n      y={0}\n      width={geometry.width}\n      height={48}\n      radius={24}\n      transition={STEPPER_LIQUID_TRANSITION}\n    >\n      <motion.output\n        {...props}\n        aria-live=\"polite\"\n        aria-atomic=\"true\"\n        style={style}\n        className={cn(\n          \"flex size-full min-w-0 items-center justify-center overflow-hidden rounded-full border border-transparent bg-transparent bg-clip-padding px-4 text-lg font-semibold tabular-nums text-foreground\",\n          className,\n        )}\n      >\n        <span className=\"sr-only\">{context.valueText}</span>\n        <span\n          aria-hidden=\"true\"\n          className=\"relative grid min-h-[1.1em] min-w-[1ch] place-items-center overflow-hidden leading-none\"\n        >\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              key={context.value}\n              initial={{\n                opacity: context.reduce ? 1 : 0.35,\n                filter: context.reduce ? \"blur(0px)\" : \"blur(2px)\",\n                transform: enterFrom,\n              }}\n              animate={{\n                opacity: 1,\n                filter: \"blur(0px)\",\n                transform: \"translateY(0%)\",\n              }}\n              exit={{\n                opacity: context.reduce ? 1 : 0,\n                filter: context.reduce ? \"blur(0px)\" : \"blur(2px)\",\n                transform: exitTo,\n                transition: {\n                  duration: context.reduce ? 0 : 0.12,\n                  ease: EASE_OUT,\n                },\n              }}\n              transition={{\n                duration: context.reduce ? 0 : 0.18,\n                ease: EASE_OUT,\n              }}\n              className=\"col-start-1 row-start-1 will-change-[transform,filter,opacity]\"\n            >\n              {renderedValue}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n      </motion.output>\n    </LiquidItem>\n  );\n}\n\nexport function AdaptiveStepperIncrement(props: AdaptiveStepperActionProps) {\n  return <StepperAction {...props} direction={1} />;\n}\n"},{"path":"components/motion/liquid.tsx","type":"registry:component","target":"@components/motion/liquid.tsx","content":"\"use client\";\n\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  forwardRef,\n  type HTMLAttributes,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype LiquidContextValue = {\n  getRoot: () => HTMLDivElement | null;\n  getPortal: () => SVGGElement | null;\n};\n\nconst LiquidContext = createContext<LiquidContextValue | null>(null);\n\nfunction useLiquidContext() {\n  const context = useContext(LiquidContext);\n  if (!context) throw new Error(\"LiquidItem must be used within <Liquid>\");\n  return context;\n}\n\nexport type LiquidEase = readonly [number, number, number, number];\n\nexport type LiquidTransition = {\n  duration?: number;\n  ease?: LiquidEase;\n};\n\nexport interface LiquidProps extends HTMLAttributes<HTMLDivElement> {\n  blur?: number;\n  contrast?: number;\n  fill?: string;\n  edgeColor?: string;\n  edgeOpacity?: number;\n  edgeWidth?: number;\n  filterPadding?: number;\n}\n\nexport const Liquid = forwardRef<HTMLDivElement, LiquidProps>(function Liquid(\n  {\n    blur = 6,\n    contrast = 18,\n    fill = \"var(--background)\",\n    edgeColor = \"var(--foreground)\",\n    edgeOpacity = 0.08,\n    edgeWidth = 1,\n    filterPadding = 24,\n    className,\n    style,\n    children,\n    ...props\n  },\n  forwardedRef: Ref<HTMLDivElement>,\n) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const portalRef = useRef<SVGGElement>(null);\n  const [size, setSize] = useState({ width: 0, height: 0 });\n  const filterId = `liquid-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const intercept = Math.round((0.5 - contrast * (5 / 12)) * 100) / 100;\n  const padding = Math.ceil(blur * 3 + filterPadding);\n\n  const setRootRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      rootRef.current = node;\n      if (typeof forwardedRef === \"function\") forwardedRef(node);\n      else if (forwardedRef) forwardedRef.current = node;\n    },\n    [forwardedRef],\n  );\n\n  useLayoutEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n\n    const measure = () => {\n      const next = {\n        width: root.offsetWidth,\n        height: root.offsetHeight,\n      };\n      setSize((current) =>\n        current.width === next.width && current.height === next.height\n          ? current\n          : next,\n      );\n    };\n\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(root);\n    return () => observer.disconnect();\n  }, []);\n\n  const context = useMemo<LiquidContextValue>(\n    () => ({\n      getRoot: () => rootRef.current,\n      getPortal: () => portalRef.current,\n    }),\n    [],\n  );\n\n  return (\n    <div\n      {...props}\n      ref={setRootRef}\n      className={cn(\"relative isolate\", className)}\n      style={style}\n    >\n      <svg\n        aria-hidden=\"true\"\n        focusable=\"false\"\n        className=\"pointer-events-none absolute inset-0 z-0 size-full overflow-visible\"\n      >\n        <defs>\n          <filter\n            id={filterId}\n            x={-padding}\n            y={-padding}\n            width={size.width + padding * 2}\n            height={size.height + padding * 2}\n            filterUnits=\"userSpaceOnUse\"\n            colorInterpolationFilters=\"sRGB\"\n          >\n            <feGaussianBlur\n              in=\"SourceGraphic\"\n              stdDeviation={blur}\n              result=\"blur\"\n            />\n            <feColorMatrix\n              in=\"blur\"\n              type=\"matrix\"\n              values={`1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 ${contrast} ${intercept}`}\n              result=\"goo\"\n            />\n            <feComposite\n              in=\"SourceGraphic\"\n              in2=\"goo\"\n              operator=\"atop\"\n              result=\"shape\"\n            />\n            {edgeWidth > 0 ? (\n              <>\n                <feColorMatrix\n                  in=\"shape\"\n                  type=\"matrix\"\n                  values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 60 -29.5\"\n                  result=\"solid-shape\"\n                />\n                <feMorphology\n                  in=\"solid-shape\"\n                  operator=\"erode\"\n                  radius={edgeWidth}\n                  result=\"inset-shape\"\n                />\n                <feComposite\n                  in=\"solid-shape\"\n                  in2=\"inset-shape\"\n                  operator=\"out\"\n                  result=\"edge-mask\"\n                />\n                <feFlood\n                  floodColor={edgeColor}\n                  floodOpacity={edgeOpacity}\n                  result=\"edge-color\"\n                />\n                <feComposite\n                  in=\"edge-color\"\n                  in2=\"edge-mask\"\n                  operator=\"in\"\n                  result=\"edge\"\n                />\n                <feMerge>\n                  <feMergeNode in=\"shape\" />\n                  <feMergeNode in=\"edge\" />\n                </feMerge>\n              </>\n            ) : null}\n          </filter>\n        </defs>\n        <g ref={portalRef} fill={fill} filter={`url(#${filterId})`} />\n      </svg>\n      <LiquidContext.Provider value={context}>\n        {children}\n      </LiquidContext.Provider>\n    </div>\n  );\n});\n\ntype LiquidBox = {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  radius: number;\n};\n\nexport interface LiquidItemProps\n  extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n  children: ReactNode;\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  radius?: number;\n  transition?: LiquidTransition;\n}\n\nfunction mix(from: number, to: number, progress: number) {\n  return from + (to - from) * progress;\n}\n\nfunction cubicBezier([x1, y1, x2, y2]: LiquidEase) {\n  return (progress: number) => {\n    if (progress <= 0) return 0;\n    if (progress >= 1) return 1;\n\n    let lower = 0;\n    let upper = 1;\n    for (let index = 0; index < 20; index++) {\n      const time = (lower + upper) / 2;\n      const inverse = 1 - time;\n      const x =\n        3 * inverse * inverse * time * x1 +\n        3 * inverse * time * time * x2 +\n        time ** 3;\n      if (x < progress) lower = time;\n      else upper = time;\n    }\n\n    const time = (lower + upper) / 2;\n    const inverse = 1 - time;\n    return (\n      3 * inverse * inverse * time * y1 +\n      3 * inverse * time * time * y2 +\n      time ** 3\n    );\n  };\n}\n\nexport function LiquidItem({\n  children,\n  x,\n  y,\n  width,\n  height,\n  radius = Math.min(width, height) / 2,\n  transition,\n  className,\n  style,\n  ...props\n}: LiquidItemProps) {\n  const context = useLiquidContext();\n  const reduce = useReducedMotion() ?? false;\n  const wrapperRef = useRef<HTMLDivElement>(null);\n  const [blob, setBlob] = useState<SVGRectElement | null>(null);\n  const currentRef = useRef<LiquidBox | null>(null);\n  const duration = reduce ? 0 : (transition?.duration ?? 280);\n  const ease = transition?.ease ?? EASE_OUT;\n  const [x1, y1, x2, y2] = ease;\n\n  useLayoutEffect(() => {\n    const portal = context.getPortal();\n    if (!portal) return;\n\n    const blob = document.createElementNS(\n      \"http://www.w3.org/2000/svg\",\n      \"rect\",\n    );\n    blob.setAttribute(\"x\", \"0\");\n    blob.setAttribute(\"y\", \"0\");\n    blob.style.transformBox = \"fill-box\";\n    blob.style.transformOrigin = \"center\";\n    blob.style.willChange = \"transform\";\n    portal.append(blob);\n    setBlob(blob);\n\n    return () => {\n      blob.remove();\n    };\n  }, [context]);\n\n  useLayoutEffect(() => {\n    const wrapper = wrapperRef.current;\n    if (!wrapper || !blob || !context.getRoot()) return;\n\n    const target = { x, y, width, height, radius };\n    const write = (box: LiquidBox) => {\n      // Keep the interactive surface and its filtered silhouette on the same\n      // frame so neither can visually outrun the other during a morph.\n      const transform = `translate(${box.x}px, ${box.y}px)`;\n      wrapper.style.transform = transform;\n      wrapper.style.width = `${box.width}px`;\n      wrapper.style.height = `${box.height}px`;\n      blob.style.transform = transform;\n      blob.setAttribute(\"width\", String(box.width));\n      blob.setAttribute(\"height\", String(box.height));\n      blob.setAttribute(\"rx\", String(box.radius));\n    };\n\n    const from = currentRef.current;\n    if (!from || duration === 0) {\n      currentRef.current = target;\n      write(target);\n      return;\n    }\n\n    const easing = cubicBezier([x1, y1, x2, y2]);\n    const startedAt = performance.now();\n    let frame = 0;\n\n    const tick = (now: number) => {\n      const progress = Math.min(1, (now - startedAt) / duration);\n      const eased = easing(progress);\n      const current = {\n        x: mix(from.x, target.x, eased),\n        y: mix(from.y, target.y, eased),\n        width: mix(from.width, target.width, eased),\n        height: mix(from.height, target.height, eased),\n        radius: mix(from.radius, target.radius, eased),\n      };\n      currentRef.current = current;\n      write(current);\n      if (progress < 1) frame = requestAnimationFrame(tick);\n    };\n\n    frame = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(frame);\n  }, [blob, context, duration, height, radius, width, x, x1, x2, y, y1, y2]);\n\n  return (\n    <div\n      {...props}\n      ref={wrapperRef}\n      className={cn(\"absolute left-0 top-0 z-10\", className)}\n      style={{ ...style, willChange: \"transform, width, height\" }}\n    >\n      {children}\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"}]}