{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tour","type":"registry:component","title":"Tour","description":"Onboarding tour that dims the page and cuts a rounded spotlight around each target, gliding and resizing it from step to step, with a card that flips and shifts to stay in the viewport, a gliding progress dot, Back/Next/Skip/Done, targets scrolled into view, Escape to close, trapped focus that returns to the trigger, controlled/uncontrolled open and step, and reduced-motion safe.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/tour.tsx","type":"registry:component","target":"@components/motion/tour.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/motion/tour\n\nimport {\n  AnimatePresence,\n  animate,\n  type MotionValue,\n  motion,\n  useIsPresent,\n  useMotionValue,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  type KeyboardEvent,\n  type ReactNode,\n  type RefObject,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Button } from \"@/components/motion/button\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TourPlacement = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport interface TourStep {\n  /** CSS selector or ref of the element to spotlight. Missing → centered card. */\n  target: string | RefObject<HTMLElement | null>;\n  title: ReactNode;\n  content: ReactNode;\n  /** Preferred side for the card. Flips/shifts to stay in the viewport. */\n  placement?: TourPlacement;\n}\n\nexport type TourCloseReason = \"complete\" | \"skip\" | \"escape\";\n\nexport interface TourLabels {\n  next?: string;\n  back?: string;\n  skip?: string;\n  done?: string;\n}\n\nexport interface TourProps {\n  steps: TourStep[];\n  /** Controlled open state. */\n  open?: boolean;\n  /** Uncontrolled initial open state. */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Controlled step index. */\n  step?: number;\n  /** Uncontrolled step the tour (re)starts at each time it opens. Default 0. */\n  defaultStep?: number;\n  onStepChange?: (step: number) => void;\n  /** Fires when Done is pressed on the last step. */\n  onComplete?: () => void;\n  /** Fires whenever the tour closes, with why. */\n  onClose?: (reason: TourCloseReason) => void;\n  /** Space between the target and the spotlight edge, px. Default 8. */\n  padding?: number;\n  /** Spotlight corner radius, px. Default 12. */\n  radius?: number;\n  /** Dim strength of the overlay, 0–1. Default 0.6. */\n  overlayOpacity?: number;\n  labels?: TourLabels;\n  /** Classes for the popover card. */\n  className?: string;\n}\n\ntype Box = { left: number; top: number; width: number; height: number };\n\n// Gap between spotlight and card, and the minimum distance the card keeps\n// from the viewport edges.\nconst GAP = 12;\nconst MARGIN = 12;\n// How long after a step change measurements keep springing toward the target\n// (the target may still be smooth-scrolling into view). Outside this window,\n// scroll/resize updates snap so the spotlight stays glued to the element.\nconst GLIDE_MS = 700;\n\nconst OPPOSITE: Record<TourPlacement, TourPlacement> = {\n  top: \"bottom\",\n  bottom: \"top\",\n  left: \"right\",\n  right: \"left\",\n};\n\nfunction resolveTarget(target: TourStep[\"target\"]): HTMLElement | null {\n  if (typeof document === \"undefined\") return null;\n  if (typeof target === \"string\") {\n    return document.querySelector<HTMLElement>(target);\n  }\n  return target.current;\n}\n\nconst clamp = (v: number, min: number, max: number) =>\n  Math.min(Math.max(v, min), Math.max(min, max));\n\n/** Positions the card beside `spot`, flipping to the side with room and\n * shifting along the edge so it never leaves the viewport. */\nfunction placeCard(\n  spot: Box | null,\n  card: { width: number; height: number },\n  preferred: TourPlacement | undefined,\n  vw: number,\n  vh: number,\n): { x: number; y: number; side: TourPlacement | \"center\" } {\n  const { width: w, height: h } = card;\n  if (!spot) {\n    return { x: (vw - w) / 2, y: (vh - h) / 2, side: \"center\" };\n  }\n  const right = spot.left + spot.width;\n  const bottom = spot.top + spot.height;\n  const space: Record<TourPlacement, number> = {\n    top: spot.top - GAP - MARGIN,\n    bottom: vh - bottom - GAP - MARGIN,\n    left: spot.left - GAP - MARGIN,\n    right: vw - right - GAP - MARGIN,\n  };\n  const need = (side: TourPlacement) =>\n    side === \"top\" || side === \"bottom\" ? h : w;\n  const order: TourPlacement[] = preferred\n    ? [\n        preferred,\n        OPPOSITE[preferred],\n        ...([\"bottom\", \"top\", \"right\", \"left\"] as const).filter(\n          (s) => s !== preferred && s !== OPPOSITE[preferred],\n        ),\n      ]\n    : [\"bottom\", \"top\", \"right\", \"left\"];\n  const side =\n    order.find((s) => space[s] >= need(s)) ??\n    order.reduce((best, s) => (space[s] > space[best] ? s : best), order[0]);\n\n  const cx = spot.left + spot.width / 2;\n  const cy = spot.top + spot.height / 2;\n  let x: number;\n  let y: number;\n  if (side === \"bottom\" || side === \"top\") {\n    x = cx - w / 2;\n    y = side === \"bottom\" ? bottom + GAP : spot.top - GAP - h;\n  } else {\n    y = cy - h / 2;\n    x = side === \"right\" ? right + GAP : spot.left - GAP - w;\n  }\n  return {\n    x: clamp(x, MARGIN, vw - w - MARGIN),\n    y: clamp(y, MARGIN, vh - h - MARGIN),\n    side,\n  };\n}\n\n/**\n * Onboarding tour: dims the page, cuts a rounded spotlight around each\n * target and glides it between steps, with a card beside the target holding\n * the step copy, progress dots and Back/Next/Skip/Done. Everything renders\n * `position: fixed` from `getBoundingClientRect`, so it stays aligned inside\n * scrolled pages and nested scroll containers.\n */\nexport function Tour({\n  steps,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  step,\n  defaultStep = 0,\n  onStepChange,\n  onComplete,\n  onClose,\n  padding = 8,\n  radius = 12,\n  overlayOpacity = 0.6,\n  labels,\n  className,\n}: TourProps) {\n  const [openState, setOpenState] = useState(defaultOpen);\n  const isOpen = (open ?? openState) && steps.length > 0;\n  const [stepState, setStepState] = useState(defaultStep);\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n\n  // Uncontrolled tours restart at `defaultStep` every time they open. Adjusted\n  // during render so the first open frame already shows the right step.\n  const [wasOpen, setWasOpen] = useState(isOpen);\n  if (wasOpen !== isOpen) {\n    setWasOpen(isOpen);\n    if (isOpen && step === undefined) setStepState(defaultStep);\n  }\n\n  const index = clamp(step ?? stepState, 0, Math.max(0, steps.length - 1));\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (open === undefined) setOpenState(next);\n      onOpenChange?.(next);\n    },\n    [open, onOpenChange],\n  );\n\n  const goTo = useCallback(\n    (next: number) => {\n      if (step === undefined) setStepState(next);\n      onStepChange?.(next);\n    },\n    [step, onStepChange],\n  );\n\n  const close = useCallback(\n    (reason: TourCloseReason) => {\n      setOpen(false);\n      if (reason === \"complete\") onComplete?.();\n      onClose?.(reason);\n    },\n    [setOpen, onComplete, onClose],\n  );\n\n  if (!mounted) return null;\n\n  return createPortal(\n    <AnimatePresence>\n      {isOpen ? (\n        <TourLayer\n          key=\"tour\"\n          steps={steps}\n          index={index}\n          goTo={goTo}\n          close={close}\n          padding={padding}\n          radius={radius}\n          overlayOpacity={overlayOpacity}\n          labels={labels}\n          className={className}\n        />\n      ) : null}\n    </AnimatePresence>,\n    document.body,\n  );\n}\n\ninterface TourLayerProps {\n  steps: TourStep[];\n  index: number;\n  goTo: (step: number) => void;\n  close: (reason: TourCloseReason) => void;\n  padding: number;\n  radius: number;\n  overlayOpacity: number;\n  labels?: TourLabels;\n  className?: string;\n}\n\nconst FOCUSABLE =\n  'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\nfunction TourLayer({\n  steps,\n  index,\n  goTo,\n  close,\n  padding,\n  radius,\n  overlayOpacity,\n  labels,\n  className,\n}: TourLayerProps) {\n  const reduce = useReducedMotion() ?? false;\n  const isPresent = useIsPresent();\n  const titleId = useId();\n  const bodyId = useId();\n  const cardRef = useRef<HTMLDivElement>(null);\n  const nextRef = useRef<HTMLButtonElement>(null);\n  const returnFocus = useRef<HTMLElement | null>(null);\n  const firstPlace = useRef(true);\n  const glideUntil = useRef(0);\n  const frame = useRef(0);\n  const [side, setSide] = useState<TourPlacement | \"center\">(\"bottom\");\n  const [hasTarget, setHasTarget] = useState(true);\n\n  const sx = useMotionValue(0);\n  const sy = useMotionValue(0);\n  const sw = useMotionValue(0);\n  const sh = useMotionValue(0);\n  const cx = useMotionValue(0);\n  const cy = useMotionValue(0);\n\n  const current = steps[index];\n  const last = index === steps.length - 1;\n  // Depend on the step's target/placement, not the step object: callers often\n  // pass an inline `steps` array, whose objects are new every render.\n  const target = current.target;\n  const placement = current.placement;\n\n  const measure = useCallback(() => {\n    const el = resolveTarget(target);\n    const vw = document.documentElement.clientWidth;\n    const vh = window.innerHeight;\n    let spot: Box | null = null;\n    if (el) {\n      const r = el.getBoundingClientRect();\n      spot = {\n        left: r.left - padding,\n        top: r.top - padding,\n        width: r.width + padding * 2,\n        height: r.height + padding * 2,\n      };\n    }\n    const glide =\n      !reduce && !firstPlace.current && performance.now() < glideUntil.current;\n    const set = (mv: MotionValue<number>, v: number) => {\n      if (glide) {\n        animate(mv, v, SPRING_LAYOUT);\n      } else {\n        mv.stop();\n        mv.set(v);\n      }\n    };\n    // With no target the spotlight collapses to a point at the center and the\n    // whole viewport stays dimmed.\n    const s = spot ?? { left: vw / 2, top: vh / 2, width: 0, height: 0 };\n    set(sx, s.left);\n    set(sy, s.top);\n    set(sw, s.width);\n    set(sh, s.height);\n\n    const card = cardRef.current;\n    const placed = placeCard(\n      spot,\n      { width: card?.offsetWidth ?? 0, height: card?.offsetHeight ?? 0 },\n      placement,\n      vw,\n      vh,\n    );\n    set(cx, placed.x);\n    set(cy, placed.y);\n    setSide(placed.side);\n    setHasTarget(Boolean(el));\n    firstPlace.current = false;\n  }, [target, placement, padding, reduce, sx, sy, sw, sh, cx, cy]);\n\n  const schedule = useCallback(() => {\n    cancelAnimationFrame(frame.current);\n    frame.current = requestAnimationFrame(measure);\n  }, [measure]);\n\n  // Step change: bring the target into view, then glide to it.\n  // biome-ignore lint/correctness/useExhaustiveDependencies: `index` re-arms the glide even when two steps share a target.\n  useLayoutEffect(() => {\n    const el = resolveTarget(target);\n    if (!firstPlace.current) glideUntil.current = performance.now() + GLIDE_MS;\n    if (el) {\n      const r = el.getBoundingClientRect();\n      const vw = document.documentElement.clientWidth;\n      const vh = window.innerHeight;\n      if (r.top < 0 || r.bottom > vh || r.left < 0 || r.right > vw) {\n        el.scrollIntoView({\n          block: \"center\",\n          inline: \"nearest\",\n          behavior: reduce ? \"auto\" : \"smooth\",\n        });\n      }\n    }\n    measure();\n  }, [index, target, measure, reduce]);\n\n  // Follow the target through scrolls (any container), resizes and reflows.\n  useEffect(() => {\n    const el = resolveTarget(target);\n    const observer = new ResizeObserver(schedule);\n    if (el) observer.observe(el);\n    if (cardRef.current) observer.observe(cardRef.current);\n    window.addEventListener(\"scroll\", schedule, true);\n    window.addEventListener(\"resize\", schedule);\n    return () => {\n      cancelAnimationFrame(frame.current);\n      observer.disconnect();\n      window.removeEventListener(\"scroll\", schedule, true);\n      window.removeEventListener(\"resize\", schedule);\n    };\n  }, [target, schedule]);\n\n  // Focus moves into the card on open and returns to whatever opened the tour\n  // the moment it starts closing.\n  useLayoutEffect(() => {\n    const active = document.activeElement;\n    returnFocus.current = active instanceof HTMLElement ? active : null;\n    nextRef.current?.focus({ preventScroll: true });\n  }, []);\n\n  useEffect(() => {\n    if (isPresent) return;\n    const target = returnFocus.current;\n    if (target?.isConnected) target.focus({ preventScroll: true });\n  }, [isPresent]);\n\n  // A step change can unmount the focused control (Back on the first step);\n  // keep focus inside the dialog.\n  // biome-ignore lint/correctness/useExhaustiveDependencies: runs after each step change.\n  useEffect(() => {\n    if (!isPresent) return;\n    const card = cardRef.current;\n    if (card && !card.contains(document.activeElement)) {\n      nextRef.current?.focus({ preventScroll: true });\n    }\n  }, [index, isPresent]);\n\n  useEffect(() => {\n    if (!isPresent) return;\n    const onKey = (event: globalThis.KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        event.preventDefault();\n        close(\"escape\");\n      }\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [close, isPresent]);\n\n  const next = () => (last ? close(\"complete\") : goTo(index + 1));\n  const back = () => index > 0 && goTo(index - 1);\n\n  const onCardKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (event.key === \"Tab\") {\n      const card = cardRef.current;\n      if (!card) return;\n      const items = Array.from(card.querySelectorAll<HTMLElement>(FOCUSABLE));\n      if (items.length === 0) {\n        event.preventDefault();\n        return;\n      }\n      const first = items[0];\n      const end = items[items.length - 1];\n      if (event.shiftKey && document.activeElement === first) {\n        event.preventDefault();\n        end.focus();\n      } else if (!event.shiftKey && document.activeElement === end) {\n        event.preventDefault();\n        first.focus();\n      }\n    } else if (event.key === \"ArrowRight\") {\n      event.preventDefault();\n      next();\n    } else if (event.key === \"ArrowLeft\") {\n      event.preventDefault();\n      back();\n    }\n  };\n\n  const enterOffset =\n    reduce || side === \"center\"\n      ? { x: 0, y: 0 }\n      : {\n          x: side === \"left\" ? 6 : side === \"right\" ? -6 : 0,\n          y: side === \"top\" ? 6 : side === \"bottom\" ? -6 : 0,\n        };\n\n  return (\n    <motion.div\n      className=\"fixed inset-0 z-[9998]\"\n      initial={{ opacity: 0 }}\n      animate={{ opacity: 1, transition: { duration: 0.2, ease: EASE_OUT } }}\n      exit={{ opacity: 0, transition: { duration: 0.14, ease: EASE_OUT } }}\n    >\n      {/* Catches pointer input so the page underneath can't be used mid-tour. */}\n      <div aria-hidden className=\"absolute inset-0\" />\n      {/* Spotlight: a rounded box whose giant box-shadow is the dim layer, so\n          the cut-out is a real rounded rect with no clip/mask geometry. Its\n          width/height are animated on purpose — the spotlight's size *is* the\n          component's layout, and it is a single paint-only element. */}\n      <motion.div\n        aria-hidden\n        className={cn(\n          \"pointer-events-none absolute left-0 top-0\",\n          // The ring would draw a dot at the center when there's no target.\n          hasTarget && \"ring-2 ring-primary/70\",\n        )}\n        style={{\n          x: sx,\n          y: sy,\n          width: sw,\n          height: sh,\n          borderRadius: radius,\n          boxShadow: `0 0 0 200vmax rgb(0 0 0 / ${overlayOpacity})`,\n        }}\n      />\n      <motion.div\n        ref={cardRef}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-labelledby={titleId}\n        aria-describedby={bodyId}\n        onKeyDown={onCardKeyDown}\n        className=\"absolute left-0 top-0 w-[min(20rem,calc(100vw-24px))] outline-none\"\n        style={{ x: cx, y: cy }}\n      >\n        <motion.div\n          initial={{ opacity: 0, scale: reduce ? 1 : 0.96, ...enterOffset }}\n          animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}\n          exit={{ opacity: 0, scale: reduce ? 1 : 0.97 }}\n          transition={reduce ? { duration: 0.15 } : SPRING_PANEL}\n          className={cn(\n            \"rounded-2xl border border-border bg-background p-4 text-foreground shadow-2xl\",\n            className,\n          )}\n        >\n          <motion.div\n            key={index}\n            aria-live=\"polite\"\n            initial={{ opacity: 0, y: reduce ? 0 : 4 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ duration: 0.22, ease: EASE_OUT }}\n          >\n            <p className=\"text-xs font-medium text-muted-foreground tabular-nums\">\n              Step {index + 1} of {steps.length}\n            </p>\n            <h2 id={titleId} className=\"mt-1 text-sm font-semibold\">\n              {current.title}\n            </h2>\n            <div\n              id={bodyId}\n              className=\"mt-1.5 text-sm leading-relaxed text-muted-foreground\"\n            >\n              {current.content}\n            </div>\n          </motion.div>\n\n          <div className=\"mt-4 flex items-center justify-between gap-3\">\n            <StepDots count={steps.length} index={index} reduce={reduce} />\n            <div className=\"flex items-center gap-1.5\">\n              {!last ? (\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={() => close(\"skip\")}\n                >\n                  {labels?.skip ?? \"Skip\"}\n                </Button>\n              ) : null}\n              {index > 0 ? (\n                <Button variant=\"outline\" size=\"sm\" onClick={back}>\n                  {labels?.back ?? \"Back\"}\n                </Button>\n              ) : null}\n              <Button ref={nextRef} size=\"sm\" onClick={next}>\n                {last ? (labels?.done ?? \"Done\") : (labels?.next ?? \"Next\")}\n              </Button>\n            </div>\n          </div>\n        </motion.div>\n      </motion.div>\n    </motion.div>\n  );\n}\n\nconst DOT = 6;\nconst DOT_GAP = 6;\n\nfunction StepDots({\n  count,\n  index,\n  reduce,\n}: {\n  count: number;\n  index: number;\n  reduce: boolean;\n}) {\n  return (\n    <div aria-hidden className=\"relative flex items-center\" style={{ gap: DOT_GAP }}>\n      {Array.from({ length: count }, (_, i) => (\n        <span\n          // biome-ignore lint/suspicious/noArrayIndexKey: dots are positional.\n          key={i}\n          className={cn(\n            \"block rounded-full transition-colors\",\n            i < index ? \"bg-primary/50\" : \"bg-muted-foreground/25\",\n          )}\n          style={{ width: DOT, height: DOT }}\n        />\n      ))}\n      <motion.span\n        className=\"absolute left-0 top-1/2 block rounded-full bg-primary\"\n        style={{ width: DOT, height: DOT, marginTop: -DOT / 2 }}\n        initial={false}\n        animate={{ x: index * (DOT + DOT_GAP), scale: 1.35 }}\n        transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n      />\n    </div>\n  );\n}\n"},{"path":"components/motion/button/index.tsx","type":"registry:component","target":"@components/motion/button/index.tsx","content":"export type {\n  ButtonLinkProps,\n  ButtonProps,\n  ButtonSize,\n  ButtonVariant,\n} from \"./base\";\nexport { Button, ButtonLink } from \"./base\";\nexport type { MagneticButtonProps } from \"./magnetic\";\nexport { MagneticButton } from \"./magnetic\";\nexport type { ButtonState, StatefulButtonProps } from \"./stateful\";\nexport { StatefulButton } from \"./stateful\";\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"},{"path":"components/motion/button/base.tsx","type":"registry:component","target":"@components/motion/button/base.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type PointerEvent,\n  type ReactNode,\n  useCallback,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { TOUCH_TARGET_CLASS } from \"@/lib/touch\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"outline\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\nexport interface ButtonProps extends Omit<\n  HTMLMotionProps<\"button\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  /** Spawn a Material-style ripple from the press point. Off by default. */\n  ripple?: boolean;\n  children?: ReactNode;\n}\n\nexport interface ButtonLinkProps extends Omit<\n  HTMLMotionProps<\"a\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  children?: ReactNode;\n}\n\ntype Ripple = { id: number; x: number; y: number; size: number };\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n  secondary: \"border border-border bg-card text-foreground hover:border-border\",\n  ghost: \"text-muted-foreground hover:text-foreground hover:bg-primary/5\",\n  outline:\n    \"border border-border bg-transparent text-foreground hover:bg-primary/5\",\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: \"h-8 px-3 text-xs gap-1.5 rounded-full\",\n  md: \"h-10 px-5 text-sm gap-2 rounded-full\",\n  lg: \"h-12 px-6 text-base gap-2 rounded-full\",\n  icon: \"h-8 w-8 rounded-lg\",\n};\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  function Button(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      ripple = false,\n      className,\n      children,\n      onPointerDown,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n    const [ripples, setRipples] = useState<Ripple[]>([]);\n    const nextId = useRef(0);\n\n    const handlePointerDown = useCallback(\n      (event: PointerEvent<HTMLButtonElement>) => {\n        if (ripple && !reduce) {\n          const rect = event.currentTarget.getBoundingClientRect();\n          const size = Math.max(rect.width, rect.height) * 2;\n          const id = nextId.current++;\n          setRipples((prev) => [\n            ...prev,\n            {\n              id,\n              x: event.clientX - rect.left,\n              y: event.clientY - rect.top,\n              size,\n            },\n          ]);\n        }\n        onPointerDown?.(event);\n      },\n      [ripple, reduce, onPointerDown],\n    );\n\n    return (\n      <motion.button\n        ref={ref}\n        type=\"button\"\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        onPointerDown={handlePointerDown}\n        className={cn(\n          \"relative inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          // The ripple layer clips itself, so on a touch screen the button can\n          // stop clipping and let its enlarged hit area overhang.\n          ripple && \"overflow-hidden pointer-coarse:overflow-visible\",\n          TOUCH_TARGET_CLASS,\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {ripple && !reduce ? (\n          <span className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]\">\n            <AnimatePresence>\n              {ripples.map((r) => (\n                <motion.span\n                  key={r.id}\n                  className=\"absolute rounded-full bg-current\"\n                  style={{\n                    left: r.x,\n                    top: r.y,\n                    width: r.size,\n                    height: r.size,\n                    x: \"-50%\",\n                    y: \"-50%\",\n                  }}\n                  initial={{ scale: 0.05, opacity: 0.3 }}\n                  animate={{ scale: 1, opacity: 0 }}\n                  exit={{ opacity: 0 }}\n                  transition={{ duration: 1.6, ease: EASE_OUT }}\n                  onAnimationComplete={() =>\n                    setRipples((prev) => prev.filter((x) => x.id !== r.id))\n                  }\n                />\n              ))}\n            </AnimatePresence>\n          </span>\n        ) : null}\n        {children}\n      </motion.button>\n    );\n  },\n);\n\nexport const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(\n  function ButtonLink(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      className,\n      children,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n\n    return (\n      <motion.a\n        ref={ref}\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        className={cn(\n          \"relative inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          TOUCH_TARGET_CLASS,\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {children}\n      </motion.a>\n    );\n  },\n);\n"},{"path":"components/motion/button/magnetic.tsx","type":"registry:component","target":"@components/motion/button/magnetic.tsx","content":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Magnetic } from \"../magnetic\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport interface MagneticButtonProps extends ButtonProps {\n  /** Magnetic pull strength. Default 0.25. */\n  strength?: number;\n  /** Class applied to the magnetic wrapper. */\n  magneticClassName?: string;\n}\n\nexport const MagneticButton = forwardRef<HTMLButtonElement, MagneticButtonProps>(function MagneticButton(\n  { strength = 0.25, magneticClassName, children, ...rest },\n  ref,\n) {\n  return (\n    <Magnetic strength={strength} className={magneticClassName}>\n      <Button ref={ref} {...rest}>\n        {children}\n      </Button>\n    </Magnetic>\n  );\n});\n"},{"path":"components/motion/button/stateful.tsx","type":"registry:component","target":"@components/motion/button/stateful.tsx","content":"\"use client\";\n\nimport { Check, Loader2, X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type ReactNode,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport type ButtonState = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nexport interface StatefulButtonProps extends Omit<ButtonProps, \"children\"> {\n  state?: ButtonState;\n  children: ReactNode;\n  loadingText?: ReactNode;\n  successText?: ReactNode;\n  errorText?: ReactNode;\n  icon?: ReactNode;\n}\n\nconst CASCADE_STAGGER = 0.025;\nconst ROLL_BLUR = \"blur(6px)\";\n\nconst CASCADE_LETTER_VARIANTS: Variants = {\n  initial: { opacity: 0, y: \"105%\", filter: ROLL_BLUR },\n  animate: (delay: number = 0) => ({\n    opacity: 1,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: { ...SPRING_SWAP, delay },\n  }),\n  exit: (delay: number = 0) => ({\n    opacity: 0,\n    y: \"-105%\",\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT, delay: delay * 0.5 },\n  }),\n};\n\nconst ICON_VARIANTS: Variants = {\n  // Width collapses too, so the icon adds/removes its own space smoothly\n  // instead of popping the row width in a single frame.\n  initial: { opacity: 0, width: 0, scale: 0.7, filter: ROLL_BLUR },\n  animate: {\n    opacity: 1,\n    width: \"1.5rem\",\n    scale: 1,\n    filter: \"blur(0px)\",\n    transition: SPRING_SWAP,\n  },\n  exit: {\n    opacity: 0,\n    width: 0,\n    scale: 0.7,\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT },\n  },\n};\n\nfunction IconSlot({ keyId, children }: { keyId: string; children: ReactNode }) {\n  const reduce = useReducedMotion();\n  return (\n    <motion.span\n      key={keyId}\n      variants={ICON_VARIANTS}\n      initial={reduce ? { opacity: 0 } : \"initial\"}\n      animate={reduce ? { opacity: 1 } : \"animate\"}\n      exit={reduce ? { opacity: 0 } : \"exit\"}\n      transition={reduce ? { duration: 0.15 } : undefined}\n      className=\"inline-grid shrink-0 place-items-center overflow-hidden\"\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nfunction TextSlot({\n  value,\n  children,\n}: {\n  value: string;\n  children: ReactNode;\n}) {\n  const reduce = useReducedMotion();\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const [width, setWidth] = useState<number>();\n  const label = typeof children === \"string\" ? children : null;\n  const cascade = label !== null && !reduce;\n\n  // Measure strings with the same per-letter layout as the cascade. Measuring\n  // the whole string preserves kerning, which can make it narrower than the\n  // inline-block letters and clip the final glyph during the width animation.\n  useLayoutEffect(() => {\n    const nextWidth = measureRef.current?.offsetWidth;\n    if (!nextWidth) return;\n    setWidth((current) => (current === nextWidth ? current : nextWidth));\n  });\n\n  return (\n    <motion.span\n      initial={false}\n      animate={{ width }}\n      transition={reduce ? { duration: 0 } : SPRING_SWAP}\n      className=\"relative inline-block overflow-hidden whitespace-nowrap align-bottom\"\n    >\n      <span\n        ref={measureRef}\n        aria-hidden\n        className=\"invisible inline-block whitespace-nowrap\"\n      >\n        {cascade\n          ? label.split(\"\").map((char, index) => (\n              <span\n                // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                key={index}\n                className=\"inline-block whitespace-pre\"\n              >\n                {char}\n              </span>\n            ))\n          : children}\n      </span>\n\n      {cascade ? (\n        <>\n          <span className=\"sr-only\">{label}</span>\n          <AnimatePresence initial={false}>\n            <motion.span\n              key={`cascade-${value}`}\n              aria-hidden\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              className=\"absolute left-0 top-0 inline-block whitespace-pre\"\n            >\n              {label.split(\"\").map((char, index) => (\n                <motion.span\n                  // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                  key={index}\n                  custom={index * CASCADE_STAGGER}\n                  variants={CASCADE_LETTER_VARIANTS}\n                  className=\"inline-block whitespace-pre will-change-[opacity,filter,transform]\"\n                >\n                  {char}\n                </motion.span>\n              ))}\n            </motion.span>\n          </AnimatePresence>\n        </>\n      ) : (\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={`text-${value}`}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 14, filter: ROLL_BLUR }}\n            animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -14, filter: ROLL_BLUR }}\n            transition={reduce ? { duration: 0.15 } : SPRING_SWAP}\n            className=\"absolute left-0 top-0 inline-block will-change-[opacity,filter,transform]\"\n          >\n            {children}\n          </motion.span>\n        </AnimatePresence>\n      )}\n    </motion.span>\n  );\n}\n\nexport const StatefulButton = forwardRef<HTMLButtonElement, StatefulButtonProps>(function StatefulButton(\n  {\n    state = \"idle\",\n    children,\n    loadingText = \"Loading\",\n    successText = \"Done\",\n    errorText = \"Try again\",\n    icon,\n    disabled,\n    ...rest\n  },\n  ref,\n) {\n  const isBusy = state === \"loading\";\n  const stateText =\n    state === \"loading\"\n      ? loadingText\n      : state === \"success\"\n        ? successText\n        : state === \"error\"\n        ? errorText\n        : children;\n  const textKey =\n    typeof stateText === \"string\" ? `${state}-${stateText}` : state;\n\n  return (\n    <Button ref={ref} disabled={disabled || isBusy} aria-busy={isBusy} whileHover={undefined} {...rest}>\n      <span\n        aria-live=\"polite\"\n        className=\"relative inline-flex items-center justify-center overflow-hidden\"\n      >\n        <AnimatePresence initial={false}>\n          {state === \"loading\" ? (\n            <IconSlot keyId=\"loading-icon\">\n              <Loader2 className=\"h-4 w-4 animate-spin\" />\n            </IconSlot>\n          ) : null}\n          {state === \"success\" ? (\n            <IconSlot keyId=\"success-icon\">\n              <Check className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n          {state === \"error\" ? (\n            <IconSlot keyId=\"error-icon\">\n              <X className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n        </AnimatePresence>\n\n        <TextSlot value={textKey}>{stateText}</TextSlot>\n\n        <AnimatePresence initial={false}>\n          {state === \"idle\" && icon ? (\n            <IconSlot keyId=\"idle-icon\">{icon}</IconSlot>\n          ) : null}\n        </AnimatePresence>\n      </span>\n    </Button>\n  );\n});\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"registry:hook","target":"@lib/hooks/use-hover-capable.ts","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"},{"path":"lib/touch.ts","type":"registry:lib","target":"@lib/touch.ts","content":"// Shared touch primitives. iOS and iPadOS run their own gestures on top of the\n// page — the long-press selection callout and the selection it drags in with\n// it — and they win: once the platform claims a touch it cancels ours\n// mid-gesture, so a press-and-hold or a drag simply dies. Surfaces that own\n// their gesture have to opt out.\n//\n// What the two classes below cover, precisely:\n// - `-webkit-touch-callout: none` stops iOS's long-press callout. WebKit-only:\n//   it is not a property other engines have, so it is inert everywhere else.\n// - `user-select: none` stops the long-press selection on every engine,\n//   Android included, and stops a drag from painting a selection under the\n//   cursor. It is inherited, so it reaches every descendant — which is why the\n//   two classes differ only in whether they apply it unconditionally.\n// What neither covers:\n// - Chrome for Android's long-press menu on a link or an image. No CSS\n//   suppresses it; a gesture surface that wraps one needs its own\n//   `onContextMenu` with `preventDefault()`.\n// - The native drag of an `<img>` or `<a>` descendant. `-webkit-user-drag` is\n//   not inherited and plain divs and buttons are not drag sources, so setting\n//   it on the surface does nothing — the child itself needs `draggable={false}`.\n\n/**\n * Classes for a surface that *is* the control: a thumb, a drum, a stage, a\n * handle, a hold button. Selection is suppressed on every input, because a\n * drag that highlights the control's own label is wrong on a mouse too.\n * Compose with `touch-none` when the surface also owns the scroll axis — leave\n * it off when the page must still scroll from there.\n */\nexport const TOUCH_GESTURE_CLASS = \"select-none [-webkit-touch-callout:none]\";\n\n/**\n * The same opt-out for a gesture surface that wraps content the consumer owns:\n * a scroller, a context-menu trigger, a sheet header, a list row. Selection is\n * suppressed only where the platform runs its own press gestures — a coarse\n * pointer — so a mouse user can still select and copy that content. If the\n * gesture itself would paint a selection under the cursor, add `select-none`\n * for the duration of the gesture rather than reaching for\n * `TOUCH_GESTURE_CLASS`.\n *\n * `pointer: coarse` describes the *primary* pointer and nothing else, so a\n * hybrid machine reads it wrong in both directions: a tablet with a mouse\n * plugged in keeps touch as primary and loses mouse selection, and a laptop\n * with a touchscreen keeps the mouse as primary and leaves selection live\n * under a finger. No media query can answer per interaction — the query is\n * about the device, and the question is about the gesture in progress. The\n * default stays here because it is right on the machines that are one thing or\n * the other, and losing a selection is a nuisance; where the miss costs a\n * *gesture* instead, the surface pairs it with `holdSelection` on the press.\n */\nexport const TOUCH_GESTURE_CONTENT_CLASS =\n  \"[-webkit-touch-callout:none] pointer-coarse:select-none\";\n\n/**\n * Grow a small control's *hit area* to at least 44×44px (WCAG 2.5.5, Apple\n * HIG) on a coarse pointer, without changing what it looks like or the space\n * it takes in layout. An invisible `::after` box, centered on the control and\n * `max(100%, 2.75rem)` on each axis, catches the taps: a 32px icon button\n * reaches 6px past each edge, a wide 40px button only grows vertically, and a\n * control already 44px or larger is left as it is. Hit-testing a pseudo\n * element lands on its owner, so handlers, focus and `:active` all behave.\n *\n * Mouse and trackpad rendering is untouched — nothing here applies outside\n * `pointer: coarse`. Requirements on the element it goes on:\n * - It must be the containing block: add `relative` (and keep it mergeable, so\n *   a consumer's `absolute` still wins through `cn`).\n * - It must not clip itself (`overflow-hidden`), or the overhang is clipped\n *   away along with its hit area.\n * - It must render children: `<input>` and other void elements have no\n *   `::after`. Put it on the wrapper or label instead.\n * - It must not already use `::after` for something visible.\n *\n * Centered 44px means two 32px buttons with a 4px gap overlap by 8px; the\n * later one wins that strip, which still ends well short of the earlier\n * button's visual center. Below a ~32px pitch the later neighbour starts\n * eating into that center — use `TOUCH_TARGET_Y_CLASS` for such dense rows.\n *\n * The overhang is real geometry: inside a scroll container it counts as\n * scrollable overflow, so a control flush against a scroller's edge can add a\n * few pixels of scroll on touch screens. Give such a scroller a little padding\n * on that side.\n */\nexport const TOUCH_TARGET_CLASS =\n  \"pointer-coarse:after:absolute pointer-coarse:after:top-1/2 pointer-coarse:after:left-1/2 pointer-coarse:after:size-[max(100%,2.75rem)] pointer-coarse:after:-translate-x-1/2 pointer-coarse:after:-translate-y-1/2 pointer-coarse:after:content-['']\";\n\n/**\n * `TOUCH_TARGET_CLASS` on the vertical axis only, for controls packed tighter\n * than ~32px apart in a row (pagination dots, dense swatches), where growing\n * sideways would hand most of each control to its neighbour. Same rules.\n */\nexport const TOUCH_TARGET_Y_CLASS =\n  \"pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:top-1/2 pointer-coarse:after:h-[max(100%,2.75rem)] pointer-coarse:after:-translate-y-1/2 pointer-coarse:after:content-['']\";\n\n/**\n * Suppress selection on `element` for as long as a gesture is running on it,\n * whatever the primary pointer of the machine happens to be. Returns the\n * release. Inline, so it wins over the class above and is gone again the\n * moment the gesture ends.\n *\n * For the press gestures a native selection would otherwise steal — a\n * long-press that opens a menu. Elsewhere prefer the classes: a surface that\n * takes selection away for the whole session is a surface whose text nobody\n * can copy.\n */\nexport function holdSelection(element: HTMLElement) {\n  element.style.setProperty(\"user-select\", \"none\");\n  element.style.setProperty(\"-webkit-user-select\", \"none\");\n  return () => {\n    element.style.removeProperty(\"user-select\");\n    element.style.removeProperty(\"-webkit-user-select\");\n  };\n}\n\n/**\n * Pointer capture, best effort. WebKit throws `NotFoundError` when the pointer\n * is already gone by the time the handler runs — routine on iOS, where the\n * system can claim the touch first — and an uncaught throw takes the rest of\n * the handler, the gesture included, down with it. Touch pointers carry\n * implicit capture anyway, so losing it is never fatal.\n */\nexport function capturePointer(element: Element, pointerId: number) {\n  try {\n    element.setPointerCapture(pointerId);\n  } catch {\n    // Pointer is no longer active — implicit capture still applies on touch.\n  }\n}\n\n/** Release a capture taken with `capturePointer`, ignoring a stale pointer. */\nexport function releasePointer(element: Element, pointerId: number) {\n  try {\n    if (element.hasPointerCapture(pointerId)) {\n      element.releasePointerCapture(pointerId);\n    }\n  } catch {\n    // Capture was already dropped by the browser.\n  }\n}\n\n/**\n * Whether this event came from a pointer that is *hovering*: not a touch, and\n * not currently pressed. Which input the user is holding right now is not\n * something a device capability can answer — a touchscreen laptop hovers and\n * taps, and iPadOS reports a fine hovering pointer for a finger — so both\n * paths stay live and each handler branches on the event it was given.\n *\n * A pen resting on the glass is making contact, not hovering: `buttons` is the\n * tell, and it sends a pen tap down the same route a finger takes.\n *\n * This answers what an *enter* asks. A leave is the other half of a pair and\n * has to be read against the enter that started it — `useHoverGesture` in\n * `lib/hooks/use-hover-gesture` does that, and hover surfaces should use it\n * rather than asking this question twice.\n */\nexport const isHoveringPointer = (event: {\n  pointerType: string;\n  buttons: number;\n}) => event.pointerType !== \"touch\" && event.buttons === 0;\n"},{"path":"components/motion/magnetic.tsx","type":"registry:component","target":"@components/motion/magnetic.tsx","content":"\"use client\";\n\nimport { motion, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { SPRING_MOUSE } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MagneticProps {\n  children: ReactNode;\n  strength?: number;\n  className?: string;\n}\n\nexport function Magnetic({ children, strength = 0.35, className }: MagneticProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  // Decorative cursor-follow: skip on touch (phantom hover) and reduced motion.\n  const enabled = !reduce && canHover;\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const sx = useSpring(x, SPRING_MOUSE);\n  const sy = useSpring(y, SPRING_MOUSE);\n\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = ref.current;\n    if (!el || !enabled) return;\n    const rect = el.getBoundingClientRect();\n    x.set((e.clientX - rect.left - rect.width / 2) * strength);\n    y.set((e.clientY - rect.top - rect.height / 2) * strength);\n  };\n\n  const onLeave = () => {\n    x.set(0);\n    y.set(0);\n  };\n\n  return (\n    <motion.div\n      ref={ref}\n      onMouseMove={onMove}\n      onMouseLeave={onLeave}\n      style={{ x: sx, y: sy }}\n      className={cn(\"inline-block\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"}]}