{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "swipe-deck",
  "type": "registry:ui",
  "title": "Swipe Deck",
  "description": "A card stack you throw left or right, with undo and matching buttons.",
  "categories": [
    "gestures"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/swipe-deck.tsx",
      "content": "\"use client\";\nimport { Tooltip } from \"@base-ui/react/tooltip\";\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useTransform, type AnimationPlaybackControls, type MotionValue } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Check, Undo, X } from \"@/lib/icons\";\nimport { ease, spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\nexport type SwipeDirection = \"left\" | \"right\";\nexport type SwipeDeckTone = \"neutral\" | \"danger\" | \"success\" | \"warning\" | \"info\";\nexport type SwipeDeckChoice = { label: string; icon?: React.ReactNode; tone?: SwipeDeckTone };\n\nexport type SwipeDeckProps<T> = Omit<React.ComponentProps<\"div\">, \"children\" | \"onChange\"> & {\n  items: T[];\n  getKey: (item: T) => string;\n  renderCard: (item: T, state: { top: boolean }) => React.ReactNode;\n  /** Names a card in announcements: \"Approved Hotel Lumen. 4 left\". */\n  getLabel?: (item: T) => string;\n  /** Position of the top card in `items`. */\n  index?: number;\n  defaultIndex?: number;\n  onIndexChange?: (index: number) => void;\n  onSwipe?: (item: T, direction: SwipeDirection) => void;\n  onUndo?: (item: T, direction: SwipeDirection) => void;\n  left?: SwipeDeckChoice;\n  right?: SwipeDeckChoice;\n  /** Shown in the deck's place once every card is decided. */\n  empty?: React.ReactNode;\n  /** The reject, undo and approve buttons under the deck. */\n  controls?: boolean;\n  disabled?: boolean;\n  /** Height of the card area in px. Cards fill it. */\n  height?: number;\n  /** Classes for the card area. */\n  deckClassName?: string;\n};\n\ntype Exit<T> = { id: string; key: string; item: T; dir: SwipeDirection; from: { x: number; y: number; rotate: number }; vx: number; vy: number; how: \"gesture\" | \"pointer\" | \"keyboard\" };\n\nconst tones: Record<SwipeDeckTone, { text: string; soft: string; stamp: string }> = {\n  neutral: { text: \"text-fg\", soft: \"bg-fg/[0.08]\", stamp: \"border-fg/25 bg-raised text-fg\" },\n  danger: { text: \"text-danger\", soft: \"bg-danger-soft\", stamp: \"border-danger/40 bg-raised text-danger\" },\n  success: { text: \"text-success\", soft: \"bg-success-soft\", stamp: \"border-success/40 bg-raised text-success\" },\n  warning: { text: \"text-warning\", soft: \"bg-warning-soft\", stamp: \"border-warning/40 bg-raised text-warning\" },\n  info: { text: \"text-info\", soft: \"bg-info-soft\", stamp: \"border-info/40 bg-raised text-info\" },\n};\n\nconst THROW = 0.34; // of the deck's width\nconst PEEK = 10; // px of each card showing under the one above\nconst clamp = (v: number, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v));\nconst back = { type: \"spring\", stiffness: 380, damping: 30, mass: 0.9 } as const;\n\nexport function SwipeDeck<T>({\n  items,\n  getKey,\n  renderCard,\n  getLabel,\n  index: indexProp,\n  defaultIndex = 0,\n  onIndexChange,\n  onSwipe,\n  onUndo,\n  left = { label: \"Skip\", tone: \"neutral\" },\n  right = { label: \"Keep\", tone: \"neutral\" },\n  empty,\n  controls = true,\n  disabled = false,\n  className,\n  deckClassName,\n  height = 380,\n  onKeyDown,\n  \"aria-label\": ariaLabel = \"Cards\",\n  ...rest\n}: SwipeDeckProps<T>) {\n  const reduce = useReducedMotion();\n  const [index, setIndex] = useControllableState({ value: indexProp, defaultValue: defaultIndex, onChange: onIndexChange });\n  const [history, setHistory] = useState<{ index: number; dir: SwipeDirection }[]>([]);\n  const [exiting, setExiting] = useState<Exit<T>[]>([]);\n  const [announcement, setAnnouncement] = useState(\"\");\n  const [dragging, setDragging] = useState(false);\n  const nonce = useRef(0);\n\n  const deckRef = useRef<HTMLDivElement>(null);\n  const width = useMotionValue(320);\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const lift = useMotionValue(1);\n  // Which way the card tips: held by its top it swings like a door on the bottom hinge, held low it tips the other way.\n  const grab = useMotionValue(1);\n  // After a card leaves, the rest of the stack still owes this much of a step forward.\n  const lag = useMotionValue(0);\n  const enter = useMotionValue(1);\n  const rotate = useTransform(() => (reduce ? 0 : (x.get() / width.get()) * 14 * grab.get()));\n  const progress = useTransform(() => clamp(Math.abs(x.get()) / (width.get() * THROW)));\n  const lean = useTransform(() => Math.sign(x.get()) * progress.get());\n  const topY = useTransform(() => y.get() + PEEK * lag.get());\n  const topScale = useTransform(() => lift.get() * (1 - 0.05 * lag.get()));\n  const topOrigin = useTransform(() => (grab.get() > 0 ? \"50% 110%\" : \"50% -10%\"));\n\n  const anims = useRef<AnimationPlaybackControls[]>([]);\n  const g = useRef({ id: -1, x0: 0, y0: 0, bx: 0, by: 0, moved: false, samples: [] as { t: number; x: number; y: number }[] });\n  const suppressClick = useRef(false);\n\n  useEffect(() => {\n    const el = deckRef.current;\n    if (!el) return;\n    const ro = new ResizeObserver(([e]) => width.set(e.contentRect.width || 320));\n    ro.observe(el);\n    return () => {\n      ro.disconnect();\n      anims.current.forEach((a) => a.stop());\n    };\n  }, [width]);\n\n  const stop = () => {\n    anims.current.forEach((a) => a.stop());\n    anims.current = [];\n  };\n  const run = (...a: AnimationPlaybackControls[]) => anims.current.push(...a);\n\n  const top = items[index];\n  // Only decisions behind the current card can be undone; a reset from outside clears the way back.\n  const undoable = history.filter((h) => h.index < index);\n  const remaining = Math.max(0, items.length - index);\n  const labelOf = (item: T) => getLabel?.(item) ?? \"Card\";\n  const choice = (dir: SwipeDirection) => (dir === \"left\" ? left : right);\n\n  function decide(dir: SwipeDirection, how: Exit<T>[\"how\"], vx = 0, vy = 0) {\n    if (!top || disabled) return;\n    stop();\n    const key = getKey(top);\n    setExiting((list) => [...list, { id: `${key}:${nonce.current++}`, key, item: top, dir, from: { x: x.get(), y: y.get(), rotate: rotate.get() }, vx, vy, how }]);\n    // The next card is already partway forward if the drag pulled it; it finishes the step from there.\n    const owed = 1 - progress.get();\n    if (reduce || how === \"keyboard\") lag.set(0);\n    else {\n      lag.set(owed);\n      run(animate(lag, 0, spring.soft));\n    }\n    x.set(0);\n    y.set(0);\n    grab.set(1);\n    lift.set(1);\n    enter.set(1);\n    setHistory([...undoable.slice(-49), { index, dir }]);\n    setIndex(index + 1);\n    onSwipe?.(top, dir);\n    const rest = remaining - 1;\n    setAnnouncement(`${choice(dir).label}: ${labelOf(top)}. ${rest ? `${rest} left` : \"No cards left\"}.`);\n  }\n\n  function undo() {\n    const last = undoable[undoable.length - 1];\n    if (!last || disabled) return;\n    const item = items[last.index];\n    if (item === undefined) return;\n    stop();\n    const key = getKey(item);\n    setExiting((list) => list.filter((e) => e.key !== key));\n    setHistory(undoable.slice(0, -1));\n    setIndex(last.index);\n    lag.set(0);\n    grab.set(1);\n    lift.set(1);\n    // It comes back from the side it left by, straightening as it lands.\n    if (reduce) {\n      x.set(0);\n      y.set(0);\n      enter.set(0);\n      run(animate(enter, 1, { duration: 0.15 }));\n    } else {\n      enter.set(1);\n      x.set((last.dir === \"right\" ? 1 : -1) * width.get() * 1.15);\n      y.set(-16);\n      run(animate(x, 0, spring.soft), animate(y, 0, spring.soft));\n    }\n    onUndo?.(item, last.dir);\n    setAnnouncement(`Restored ${labelOf(item)}.`);\n  }\n\n  // ---- pointer --------------------------------------------------------------------------\n\n  function onPointerDown(e: React.PointerEvent<HTMLDivElement>) {\n    if (disabled || (e.pointerType === \"mouse\" && e.button !== 0)) return;\n    stop();\n    lag.set(0);\n    const rect = e.currentTarget.getBoundingClientRect();\n    if (Math.abs(x.get()) < 2) grab.set(e.clientY - rect.top < rect.height * 0.6 ? 1 : -1);\n    g.current = { id: e.pointerId, x0: e.clientX, y0: e.clientY, bx: x.get(), by: y.get(), moved: false, samples: [] };\n    e.currentTarget.setPointerCapture(e.pointerId);\n    if (!reduce) run(animate(lift, 1.02, spring.snappy));\n  }\n\n  function onPointerMove(e: React.PointerEvent<HTMLDivElement>) {\n    const s = g.current;\n    if (s.id !== e.pointerId) return;\n    const dx = e.clientX - s.x0;\n    const dy = e.clientY - s.y0;\n    if (!s.moved) {\n      if (Math.hypot(dx, dy) < 4) return;\n      s.moved = true;\n      setDragging(true);\n    }\n    x.set(s.bx + dx);\n    // Vertical travel is damped: the deck is about left and right.\n    y.set(s.by + dy * 0.3);\n    s.samples.push({ t: performance.now(), x: x.get(), y: y.get() });\n    if (s.samples.length > 10) s.samples.shift();\n  }\n\n  function onPointerEnd(e: React.PointerEvent<HTMLDivElement>) {\n    const s = g.current;\n    if (s.id !== e.pointerId) return;\n    s.id = -1;\n    setDragging(false);\n    if (!s.moved) {\n      if (!reduce) run(animate(lift, 1, spring.snappy));\n      return;\n    }\n    suppressClick.current = true;\n    const now = performance.now();\n    const recent = s.samples.filter((p) => now - p.t < 100);\n    let vx = 0;\n    let vy = 0;\n    if (recent.length > 1 && e.type !== \"pointercancel\") {\n      const a = recent[0];\n      const b = recent[recent.length - 1];\n      const dt = Math.max(1, b.t - a.t);\n      vx = ((b.x - a.x) / dt) * 1000;\n      vy = ((b.y - a.y) / dt) * 1000;\n    }\n    const px = x.get();\n    const far = Math.abs(px) > width.get() * THROW;\n    const flung = Math.abs(vx) > 600 && Math.sign(vx) === Math.sign(px) && Math.abs(px) > 24;\n    if (e.type !== \"pointercancel\" && (far || flung)) return decide(px > 0 ? \"right\" : \"left\", \"gesture\", vx, vy);\n    // Not far enough: it springs home carrying the release speed, so a half-hearted flick settles, not snaps.\n    if (reduce) {\n      x.set(0);\n      y.set(0);\n      lift.set(1);\n    } else run(animate(x, 0, { ...back, velocity: vx }), animate(y, 0, { ...back, velocity: vy }), animate(lift, 1, spring.snappy));\n  }\n\n  const visible = items.slice(index, index + 4);\n\n  return (\n    <div className={cn(\"flex w-full max-w-[320px] flex-col items-center gap-5\", className)} {...rest}>\n      <div\n        ref={deckRef}\n        tabIndex={disabled ? -1 : 0}\n        role=\"group\"\n        aria-roledescription=\"card stack\"\n        aria-label={ariaLabel}\n        aria-keyshortcuts=\"ArrowLeft ArrowRight Backspace\"\n        data-state={dragging ? \"dragging\" : top ? \"idle\" : \"empty\"}\n        data-disabled={disabled || undefined}\n        onKeyDown={(e) => {\n          onKeyDown?.(e);\n          if (e.defaultPrevented) return;\n          if (e.key === \"ArrowLeft\" || e.key === \"ArrowRight\") {\n            e.preventDefault();\n            e.stopPropagation();\n            decide(e.key === \"ArrowLeft\" ? \"left\" : \"right\", \"keyboard\");\n          } else if (e.key === \"Backspace\" || ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === \"z\")) {\n            e.preventDefault();\n            e.stopPropagation();\n            undo();\n          }\n        }}\n        className={cn(\n          \"relative w-full rounded-2xl outline-none\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-4 focus-visible:outline-fg-3\",\n          deckClassName,\n        )}\n        style={{ height, marginBottom: PEEK * 2 }}\n      >\n        {/* Back to front, so the top card paints last. */}\n        {visible\n          .map((item, depth) => ({ item, depth }))\n          .reverse()\n          .map(({ item, depth }) =>\n            depth === 0 ? (\n              <motion.div\n                key={getKey(item)}\n                data-top=\"\"\n                style={{ x, y: topY, rotate, scale: topScale, opacity: enter, transformOrigin: topOrigin, zIndex: 10 }}\n                onPointerDown={onPointerDown}\n                onPointerMove={onPointerMove}\n                onPointerUp={onPointerEnd}\n                onPointerCancel={onPointerEnd}\n                onDragStart={(e) => e.preventDefault()}\n                onClickCapture={(e) => {\n                  if (suppressClick.current) {\n                    e.preventDefault();\n                    e.stopPropagation();\n                  }\n                  suppressClick.current = false;\n                }}\n                className={cn(\n                  \"absolute inset-0 touch-pan-y select-none\",\n                  disabled ? \"cursor-default\" : dragging ? \"cursor-grabbing\" : \"cursor-grab\",\n                )}\n              >\n                <CardFrame>\n                  {renderCard(item, { top: true })}\n                  <Stamp choice={right} side=\"right\" lean={lean} />\n                  <Stamp choice={left} side=\"left\" lean={lean} />\n                </CardFrame>\n              </motion.div>\n            ) : (\n              <BackCard key={getKey(item)} depth={depth} progress={progress} lag={lag}>\n                {renderCard(item, { top: false })}\n              </BackCard>\n            ),\n          )}\n\n        {exiting.map((ex) => (\n          <ExitCard\n            key={ex.id}\n            exit={ex}\n            width={width.get()}\n            reduce={!!reduce}\n            onDone={() => setExiting((list) => list.filter((e) => e.id !== ex.id))}\n          >\n            <CardFrame>\n              {renderCard(ex.item, { top: false })}\n              <Stamp choice={choice(ex.dir)} side={ex.dir} lean={ex.dir === \"right\" ? 1 : -1} />\n            </CardFrame>\n          </ExitCard>\n        ))}\n\n        <AnimatePresence initial={false}>\n          {!top && empty && (\n            <motion.div\n              key=\"empty\"\n              initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96, filter: \"blur(2px)\" }}\n              animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, transition: { duration: 0.1 } }}\n              transition={{ duration: 0.3, ease: ease.out, delay: reduce ? 0 : 0.12 }}\n              className=\"absolute inset-0 grid place-items-center rounded-2xl border border-dashed border-line-2\"\n            >\n              {empty}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n\n      {controls && (\n        <Tooltip.Provider delay={500}>\n          <div className=\"flex items-center gap-4\">\n            <DeckButton label={left.label} shortcut=\"←\" tone={left.tone ?? \"neutral\"} size=\"lg\" lean={lean} side=\"left\" disabled={disabled || !top} onClick={() => decide(\"left\", \"pointer\")}>\n              {left.icon ?? <X size={18} />}\n            </DeckButton>\n            <DeckButton label=\"Undo\" shortcut=\"⌫\" tone=\"neutral\" size=\"sm\" disabled={disabled || undoable.length === 0} onClick={undo}>\n              <Undo />\n            </DeckButton>\n            <DeckButton label={right.label} shortcut=\"→\" tone={right.tone ?? \"neutral\"} size=\"lg\" lean={lean} side=\"right\" disabled={disabled || !top} onClick={() => decide(\"right\", \"pointer\")}>\n              {right.icon ?? <Check size={18} />}\n            </DeckButton>\n          </div>\n        </Tooltip.Provider>\n      )}\n\n      <p role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </p>\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Pieces\n * -----------------------------------------------------------------------------------------------*/\n\nfunction CardFrame({ children }: { children: React.ReactNode }) {\n  return <div className=\"relative h-full w-full overflow-hidden rounded-2xl border border-line-2 bg-raised shadow-pop\">{children}</div>;\n}\n\n/** A card waiting its turn. It steps forward as the top card is dragged away. */\nfunction BackCard({ depth, progress, lag, children }: { depth: number; progress: MotionValue<number>; lag: MotionValue<number>; children: React.ReactNode }) {\n  const d = useTransform(() => Math.max(0, depth + lag.get() - progress.get()));\n  const scale = useTransform(() => 1 - 0.05 * d.get());\n  const y = useTransform(() => PEEK * d.get());\n  const opacity = useTransform(() => {\n    const v = d.get();\n    return v <= 1 ? 1 : v <= 2 ? 1 - (v - 1) * 0.45 : 0.55 * clamp(3 - v);\n  });\n  return (\n    <motion.div aria-hidden inert style={{ scale, y, opacity, zIndex: 10 - depth, transformOrigin: \"50% 100%\" }} className=\"pointer-events-none absolute inset-0\">\n      <CardFrame>{children}</CardFrame>\n    </motion.div>\n  );\n}\n\nfunction ExitCard<T>({ exit, width, reduce, onDone, children }: { exit: Exit<T>; width: number; reduce: boolean; onDone: () => void; children: React.ReactNode }) {\n  const x = useMotionValue(exit.from.x);\n  const y = useMotionValue(exit.from.y);\n  const rotate = useMotionValue(exit.from.rotate);\n  const opacity = useMotionValue(1);\n  const done = useRef(onDone);\n\n  useEffect(() => {\n    done.current = onDone;\n  });\n\n  useEffect(() => {\n    const sign = exit.dir === \"right\" ? 1 : -1;\n    const to = sign * (width * 1.25 + 80);\n    let a: AnimationPlaybackControls[];\n    if (reduce) a = [animate(opacity, 0, { duration: 0.16 })];\n    else if (exit.how === \"gesture\") {\n      // Leaves along the throw, never slower than it was let go.\n      const vx = sign * Math.max(Math.abs(exit.vx), 900);\n      a = [\n        animate(x, to, { type: \"spring\", stiffness: 120, damping: 20, velocity: vx, restDelta: 40 }),\n        animate(y, exit.from.y + exit.vy * 0.12, { type: \"spring\", stiffness: 120, damping: 20, velocity: exit.vy }),\n        animate(rotate, exit.from.rotate + sign * 10, { duration: 0.4, ease: ease.out }),\n      ];\n    } else {\n      const d = exit.how === \"keyboard\" ? 0.2 : 0.34;\n      a = [\n        animate(x, to, { duration: d, ease: ease.in }),\n        animate(y, [0, -18, 12], { duration: d, ease: \"easeOut\" }),\n        animate(rotate, sign * 16, { duration: d, ease: ease.in }),\n      ];\n    }\n    let live = true;\n    a[0].finished.then(() => live && done.current());\n    return () => {\n      live = false;\n      a.forEach((c) => c.stop());\n    };\n  }, [exit, width, reduce, x, y, rotate, opacity]);\n\n  return (\n    <motion.div aria-hidden inert style={{ x, y, rotate, opacity, zIndex: 20, transformOrigin: \"50% 110%\" }} className=\"pointer-events-none absolute inset-0\">\n      {children}\n    </motion.div>\n  );\n}\n\n/** The verdict, written on the card as it leans toward that side. */\nfunction Stamp({ choice, side, lean }: { choice: SwipeDeckChoice; side: SwipeDirection; lean: MotionValue<number> | number }) {\n  const t = tones[choice.tone ?? \"neutral\"];\n  const from = (v: number) => clamp(((side === \"right\" ? v : -v) - 0.2) / 0.6);\n  const value = typeof lean === \"number\" ? from(lean) : undefined;\n  const mv = useTransform(() => (typeof lean === \"number\" ? from(lean) : from(lean.get())));\n  const scale = useTransform(() => 0.9 + 0.1 * mv.get());\n  return (\n    <motion.span\n      aria-hidden\n      style={{ opacity: value ?? mv, scale: value !== undefined ? 1 : scale }}\n      className={cn(\n        \"pointer-events-none absolute top-4 flex h-7 items-center gap-1.5 rounded-full border px-2.5 text-[12.5px] font-medium shadow-[var(--shadow)]\",\n        // It sits on the trailing edge, where it stays readable while the card travels.\n        side === \"right\" ? \"left-4 origin-left\" : \"right-4 origin-right\",\n        t.stamp,\n      )}\n    >\n      <span className=\"grid size-3.5 place-items-center [&>svg]:size-3.5\">{choice.icon ?? (side === \"right\" ? <Check /> : <X />)}</span>\n      {choice.label}\n    </motion.span>\n  );\n}\n\nfunction DeckButton({\n  label,\n  shortcut,\n  tone,\n  size,\n  lean,\n  side,\n  disabled,\n  onClick,\n  children,\n}: {\n  label: string;\n  shortcut: string;\n  tone: SwipeDeckTone;\n  size: \"sm\" | \"lg\";\n  lean?: MotionValue<number>;\n  side?: SwipeDirection;\n  disabled: boolean;\n  onClick: () => void;\n  children: React.ReactNode;\n}) {\n  const t = tones[tone];\n  // The button a drag would press lights up as the card leans its way.\n  const fill = useTransform(() => (lean && side ? clamp((side === \"right\" ? lean.get() : -lean.get()) * 1.1) : 0));\n  const grow = useTransform(() => 1 + 0.08 * fill.get());\n  return (\n    <Tooltip.Root>\n      <Tooltip.Trigger\n        render={\n          <button\n            type=\"button\"\n            aria-label={label}\n            aria-keyshortcuts={shortcut === \"←\" ? \"ArrowLeft\" : shortcut === \"→\" ? \"ArrowRight\" : \"Backspace\"}\n            disabled={disabled}\n            onClick={onClick}\n            className={cn(\n              \"relative grid shrink-0 place-items-center overflow-hidden rounded-full border border-line-2 bg-raised shadow-[var(--shadow)] outline-none\",\n              \"transition-[background-color,border-color,scale,opacity] duration-150 hover:border-fg-4 active:scale-[0.92] active:duration-75\",\n              \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n              \"disabled:pointer-events-none disabled:opacity-40\",\n              size === \"lg\" ? cn(\"size-12\", t.text) : \"size-9 text-fg-2 hover:text-fg\",\n            )}\n          />\n        }\n      >\n        {size === \"lg\" && <motion.span aria-hidden style={{ opacity: fill }} className={cn(\"absolute inset-0\", t.soft)} />}\n        <motion.span style={{ scale: grow }} className=\"relative grid place-items-center\">\n          {children}\n        </motion.span>\n      </Tooltip.Trigger>\n      <Tooltip.Portal>\n        <Tooltip.Positioner side=\"bottom\" sideOffset={8} collisionPadding={8} className=\"z-(--z-tooltip)\">\n          <Tooltip.Popup\n            className={cn(\n              \"flex origin-(--transform-origin) items-center gap-2 rounded-lg border border-line-2 bg-raised py-[3px] pl-2 pr-1 text-[12px] leading-5 text-fg shadow-pop\",\n              \"transition-[opacity,scale] duration-150 ease-out-expo data-ending-style:duration-100\",\n              \"data-starting-style:scale-96 data-starting-style:opacity-0 data-ending-style:opacity-0 data-instant:transition-none\",\n            )}\n          >\n            {label}\n            <kbd className=\"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[5px] border border-line-2 bg-frame px-1 font-sans text-[11px] leading-none text-fg-2\">{shortcut}</kbd>\n          </Tooltip.Popup>\n        </Tooltip.Positioner>\n      </Tooltip.Portal>\n    </Tooltip.Root>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/swipe-deck"
}
