{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card-deck",
  "title": "Card Deck",
  "description": "A one-at-a-time deck for an argument told in order: a swipeable strip, a pile, story tap zones or titles beside the slide, with an optional rewind.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/card-deck.tsx",
      "content": "import type * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport {\n  DeckControls,\n  DEFAULT_LABELS,\n  type CardDeckLabels,\n  type CardDeckSlide,\n  type CardDeckStat,\n} from \"@/registry/miseui/ui/card-deck-parts\"\nimport { OutlineStage, StackStage, StoryStage, StripStage } from \"@/registry/miseui/ui/card-deck-stages\"\nimport { useDeckState, useDeckSwipe } from \"@/registry/miseui/ui/card-deck-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype CardDeckVariant = \"slides\" | \"stack\" | \"story\" | \"outline\"\ntype CardDeckEffect = \"rewind\"\n\nconst STAGE = { slides: StripStage, stack: StackStage, story: StoryStage, outline: OutlineStage } as const\n\ntype RootAttributes = Omit<React.ComponentProps<\"section\">, \"children\" | \"onKeyDown\">\n\ninterface CardDeckProps extends RootAttributes {\n  /** What this deck is, read out when someone enters it. */\n  label: string\n  slides: readonly CardDeckSlide[]\n  /** `slides` a strip you swipe, `stack` a pile you throw from, `story` tap zones and segments, `outline` titles beside the slide. */\n  variant?: CardDeckVariant\n  /** `rewind` adds Start over: the deck runs back through every slide passed, so the way back reads as the way back. */\n  effect?: CardDeckEffect\n  index?: number\n  defaultIndex?: number\n  /** Fires for each slide a rewind passes, so a controlled deck can follow it. */\n  onIndexChange?: (index: number) => void\n  labels?: Partial<CardDeckLabels>\n}\n\nfunction CardDeck({\n  label,\n  slides,\n  variant = \"slides\",\n  effect,\n  index,\n  defaultIndex = 0,\n  onIndexChange,\n  labels,\n  className,\n  ...props\n}: CardDeckProps) {\n  const reduceMotion = useReducedMotion() ?? false\n  const rewindable = effect === \"rewind\"\n  const merged = { ...DEFAULT_LABELS, ...labels }\n  const deck = useDeckState({ count: slides.length, index, defaultIndex, onIndexChange, rewindable, reduceMotion })\n  const swipe = useDeckSwipe(deck, reduceMotion)\n  const Stage = STAGE[variant]\n  const current = slides[deck.active]\n  return (\n    <section\n      aria-roledescription=\"slide deck\"\n      aria-label={label}\n      data-slot=\"card-deck\"\n      data-variant={variant}\n      data-effect={effect}\n      className={cn(\"@container grid w-full min-w-0 grid-cols-[minmax(0,1fr)] gap-3\", className)}\n      onKeyDown={deck.onKeyDown}\n      {...props}\n    >\n      <Stage slides={slides} deck={deck} swipe={swipe} labels={merged} rewindable={rewindable} />\n      {variant === \"story\" ? null : <DeckControls deck={deck} labels={merged} rewindable={rewindable} />}\n      <p className=\"sr-only\" aria-live={deck.rewinding ? \"off\" : \"polite\"} aria-atomic=\"true\">\n        {current ? merged.slide(deck.active + 1, slides.length, current.title) : \"\"}\n      </p>\n    </section>\n  )\n}\n\nexport {\n  CardDeck,\n  type CardDeckProps,\n  type CardDeckSlide,\n  type CardDeckStat,\n  type CardDeckVariant,\n  type CardDeckEffect,\n  type CardDeckLabels,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/card-deck-parts.tsx",
      "content": "import type * as React from \"react\"\nimport { ChevronLeft, ChevronRight, RotateCcw } from \"lucide-react\"\nimport type { DeckState } from \"@/registry/miseui/ui/card-deck-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nexport interface CardDeckStat {\n  label: string\n  value: string\n}\n\nexport interface CardDeckSlide {\n  id: string\n  title: string\n  /** A short line above the title, such as a section name. */\n  kicker?: string\n  body?: React.ReactNode\n  /** Up to three figures; the label is read first and the value is seen first. */\n  stats?: readonly CardDeckStat[]\n  media?: React.ReactNode\n}\n\nexport interface CardDeckLabels {\n  previous: string\n  next: string\n  restart: string\n  slide: (position: number, count: number, title: string) => string\n}\n\nexport const DEFAULT_LABELS: CardDeckLabels = {\n  previous: \"Previous slide\",\n  next: \"Next slide\",\n  restart: \"Start over\",\n  slide: (position, count, title) => `Slide ${position} of ${count}: ${title}`,\n}\n\ninterface FaceProps {\n  slide: CardDeckSlide\n  /** `inverted` sits on the foreground colour, where the muted token would lose contrast. */\n  tone?: \"plain\" | \"inverted\"\n  large?: boolean\n}\n\nfunction DeckStats({ stats, quiet }: { stats: readonly CardDeckStat[]; quiet: string }) {\n  return (\n    <dl className=\"grid grid-cols-[repeat(auto-fit,minmax(5rem,1fr))] gap-4\">\n      {stats.map((stat) => (\n        <div key={stat.label} className=\"flex min-w-0 flex-col-reverse gap-1\">\n          <dt className={cn(\"text-[13px]\", quiet)}>{stat.label}</dt>\n          <dd className=\"text-xl font-semibold tracking-tight tabular-nums\">{stat.value}</dd>\n        </div>\n      ))}\n    </dl>\n  )\n}\n\nexport function DeckFace({ slide, tone = \"plain\", large = false }: FaceProps) {\n  const quiet = tone === \"inverted\" ? \"text-background/70\" : \"text-muted-foreground\"\n  return (\n    <div data-slot=\"card-deck-face\" className=\"flex h-full min-h-56 min-w-0 flex-col justify-between gap-6 p-6\">\n      {slide.kicker ? <p className={cn(\"text-[13px]\", quiet)}>{slide.kicker}</p> : <span />}\n      {slide.media ? <div className=\"overflow-hidden rounded-lg\">{slide.media}</div> : null}\n      <div className=\"grid min-w-0 gap-2\">\n        <h3 className={cn(\"font-semibold tracking-tight text-balance\", large ? \"text-2xl\" : \"text-xl\")}>{slide.title}</h3>\n        {slide.body ? <div className={cn(\"text-sm leading-relaxed\", quiet)}>{slide.body}</div> : null}\n        {slide.stats?.length ? (\n          <div className=\"pt-2\">\n            <DeckStats stats={slide.stats} quiet={quiet} />\n          </div>\n        ) : null}\n      </div>\n    </div>\n  )\n}\n\nconst ICON_BUTTON_CLASS = [\n  \"inline-grid size-10 shrink-0 place-items-center rounded-full border bg-background text-foreground outline-none\",\n  \"transition-colors duration-(--duration-fast) ease-(--ease-mise) hover:bg-muted\",\n  \"focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n  \"aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:bg-background\",\n]\n\ntype DeckButtonProps = Omit<React.ComponentProps<\"button\">, \"disabled\"> & { inactive?: boolean }\n\n/** aria-disabled rather than disabled: a button that disables itself under the finger would drop keyboard focus to the page. */\nexport function DeckButton({ inactive = false, onClick, className, ...props }: DeckButtonProps) {\n  return (\n    <button\n      type=\"button\"\n      aria-disabled={inactive || undefined}\n      onClick={inactive ? undefined : onClick}\n      className={cn(ICON_BUTTON_CLASS, className)}\n      {...props}\n    />\n  )\n}\n\ninterface ControlsProps {\n  deck: DeckState\n  labels: CardDeckLabels\n  rewindable: boolean\n}\n\nexport function DeckRestart({ deck, labels, className }: { deck: DeckState; labels: CardDeckLabels; className?: string }) {\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"card-deck-restart\"\n      aria-disabled={deck.active === 0 || undefined}\n      onClick={deck.active === 0 ? undefined : deck.rewind}\n      className={cn(\n        \"inline-flex h-10 items-center gap-2 rounded-full px-3 text-[13px] font-medium outline-none\",\n        \"transition-colors duration-(--duration-fast) ease-(--ease-mise) hover:bg-muted\",\n        \"focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:bg-transparent\",\n        className,\n      )}\n    >\n      <RotateCcw aria-hidden=\"true\" className=\"size-4\" />\n      {labels.restart}\n    </button>\n  )\n}\n\nexport function DeckControls({ deck, labels, rewindable }: ControlsProps) {\n  return (\n    <div data-slot=\"card-deck-controls\" className=\"flex flex-wrap items-center justify-between gap-2\">\n      <div className=\"flex items-center gap-2\">\n        <DeckButton aria-label={labels.previous} inactive={deck.active === 0} onClick={() => deck.go(deck.active - 1)}>\n          <ChevronLeft aria-hidden=\"true\" className=\"size-4\" />\n        </DeckButton>\n        <DeckButton aria-label={labels.next} inactive={deck.active === deck.last} onClick={() => deck.go(deck.active + 1)}>\n          <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n        </DeckButton>\n        <span aria-hidden=\"true\" className=\"pl-2 text-[13px] text-muted-foreground tabular-nums\">\n          {deck.active + 1} / {deck.count}\n        </span>\n      </div>\n      {rewindable ? <DeckRestart deck={deck} labels={labels} /> : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/card-deck-stages.tsx",
      "content": "import { motion, useTransform } from \"motion/react\"\nimport { DeckButton, DeckFace, DeckRestart, type CardDeckLabels, type CardDeckSlide } from \"@/registry/miseui/ui/card-deck-parts\"\nimport type { DeckState, DeckSwipe } from \"@/registry/miseui/ui/card-deck-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nexport interface StageProps {\n  slides: readonly CardDeckSlide[]\n  deck: DeckState\n  swipe: DeckSwipe\n  labels: CardDeckLabels\n  rewindable: boolean\n}\n\nconst STACK_DEPTH = 2\nconst STACK_STEP_PX = 12\nconst STACK_SHRINK = 0.04\nconst DRAG_TILT_DEG = 4\nconst DRAG_TILT_RANGE_PX = 240\n\n/** One pose per place in the pile: gone to the left, on top, or waiting behind with its bottom edge showing. */\nexport function stackPose(position: number) {\n  if (position < 0) return { x: \"-112%\", y: 0, scale: 1, rotate: -6, opacity: 0 }\n  const depth = Math.min(position, STACK_DEPTH + 1)\n  return {\n    x: \"0%\",\n    y: depth * STACK_STEP_PX,\n    scale: 1 - depth * STACK_SHRINK,\n    rotate: 0,\n    opacity: position > STACK_DEPTH ? 0 : 1,\n  }\n}\n\nconst SURFACE_CLASS = \"rounded-xl border bg-card text-card-foreground shadow-[0_8px_24px_-16px_rgb(0_0_0/0.35)]\"\nconst GRAB_CLASS = \"cursor-grab touch-pan-y select-none active:cursor-grabbing\"\n\nexport function StripStage({ slides, deck, swipe }: StageProps) {\n  return (\n    <div\n      data-slot=\"card-deck-stage\"\n      className={cn(\"relative min-w-0 overflow-hidden rounded-xl border bg-card text-card-foreground\", GRAB_CLASS)}\n      {...swipe.handlers}\n    >\n      <motion.div style={{ x: swipe.dragX }}>\n        {/* A percentage is of the strip's own width, so a resize needs no measuring and no correction. */}\n        <motion.div\n          data-slot=\"card-deck-strip\"\n          className=\"flex\"\n          initial={false}\n          animate={{ x: `${-deck.active * 100}%` }}\n          transition={deck.transition}\n        >\n          {slides.map((slide, position) => (\n            <div key={slide.id} data-slot=\"card-deck-slide\" className=\"w-full shrink-0\" inert={position !== deck.active}>\n              <DeckFace slide={slide} />\n            </div>\n          ))}\n        </motion.div>\n      </motion.div>\n      <div className=\"absolute inset-x-0 bottom-0 h-0.5 bg-border\">\n        <motion.div\n          data-slot=\"card-deck-progress\"\n          className=\"h-full origin-left bg-mise\"\n          initial={false}\n          animate={{ scaleX: (deck.active + 1) / Math.max(deck.count, 1) }}\n          transition={deck.transition}\n        />\n      </div>\n    </div>\n  )\n}\n\nexport function StackStage({ slides, deck, swipe }: StageProps) {\n  const tilt = useTransform(swipe.dragX, [-DRAG_TILT_RANGE_PX, DRAG_TILT_RANGE_PX], [-DRAG_TILT_DEG, DRAG_TILT_DEG])\n  return (\n    <div\n      data-slot=\"card-deck-stage\"\n      data-layout=\"pile\"\n      className={cn(\"grid min-w-0 grid-cols-[minmax(0,1fr)] overflow-x-clip pb-6 [&>*]:[grid-area:1/1]\", GRAB_CLASS)}\n      {...swipe.handlers}\n    >\n      {slides.map((slide, position) => {\n        const place = position - deck.active\n        return (\n          <motion.div\n            key={slide.id}\n            data-slot=\"card-deck-slide\"\n            data-place={place}\n            className={cn(\"min-w-0 origin-bottom\", place !== 0 && \"pointer-events-none\")}\n            style={{ zIndex: deck.count - position }}\n            inert={place !== 0}\n            initial={false}\n            animate={stackPose(place)}\n            transition={deck.transition}\n          >\n            <motion.div\n              className={cn(\"h-full\", SURFACE_CLASS)}\n              style={place === 0 ? { x: swipe.dragX, rotate: tilt } : undefined}\n            >\n              <DeckFace slide={slide} />\n            </motion.div>\n          </motion.div>\n        )\n      })}\n    </div>\n  )\n}\n\nfunction CrossfadeSlides({ slides, deck, inverted = false }: { slides: readonly CardDeckSlide[]; deck: DeckState; inverted?: boolean }) {\n  return (\n    <div className=\"grid min-w-0 grid-cols-[minmax(0,1fr)] [&>*]:[grid-area:1/1]\">\n      {slides.map((slide, position) => (\n        <motion.div\n          key={slide.id}\n          data-slot=\"card-deck-slide\"\n          className=\"min-w-0\"\n          inert={position !== deck.active}\n          initial={false}\n          animate={{ opacity: position === deck.active ? 1 : 0 }}\n          transition={deck.transition}\n        >\n          <DeckFace slide={slide} tone={inverted ? \"inverted\" : \"plain\"} large={inverted} />\n        </motion.div>\n      ))}\n    </div>\n  )\n}\n\nexport function StoryStage({ slides, deck, labels, rewindable }: StageProps) {\n  const atEnd = deck.active === deck.last\n  return (\n    <div data-slot=\"card-deck-stage\" className=\"relative grid min-w-0 grid-cols-[minmax(0,1fr)] overflow-hidden rounded-xl bg-foreground text-background\">\n      <div data-slot=\"card-deck-segments\" aria-hidden=\"true\" className=\"flex gap-1 p-3\">\n        {slides.map((slide, position) => (\n          <span key={slide.id} className=\"h-0.5 min-w-0 flex-1 overflow-hidden rounded-full bg-background/25\">\n            <motion.span\n              data-seen={position <= deck.active}\n              className=\"block h-full origin-left bg-background\"\n              initial={false}\n              animate={{ scaleX: position <= deck.active ? 1 : 0 }}\n              transition={deck.transition}\n            />\n          </span>\n        ))}\n      </div>\n      <CrossfadeSlides slides={slides} deck={deck} inverted />\n      {/* Reading is forward, so the larger zone goes forward. The plain button styles are stripped so the card stays one surface. */}\n      <DeckButton\n        aria-label={labels.previous}\n        inactive={deck.active === 0}\n        onClick={() => deck.go(deck.active - 1)}\n        className=\"absolute inset-y-0 left-0 z-10 h-auto w-1/3 rounded-none border-0 bg-transparent hover:bg-transparent aria-disabled:hover:bg-transparent focus-visible:ring-inset\"\n      />\n      <DeckButton\n        aria-label={labels.next}\n        inactive={atEnd}\n        onClick={() => deck.go(deck.active + 1)}\n        className=\"absolute inset-y-0 right-0 z-10 h-auto w-2/3 rounded-none border-0 bg-transparent hover:bg-transparent aria-disabled:hover:bg-transparent focus-visible:ring-inset\"\n      />\n      {rewindable && atEnd ? (\n        <DeckRestart deck={deck} labels={labels} className=\"absolute top-8 right-3 z-20 hover:bg-background/15\" />\n      ) : null}\n    </div>\n  )\n}\n\nexport function OutlineStage({ slides, deck }: StageProps) {\n  return (\n    <div data-slot=\"card-deck-stage\" className=\"grid min-w-0 grid-cols-[minmax(0,1fr)] gap-4 @md:grid-cols-[minmax(0,10rem)_minmax(0,1fr)]\">\n      <div className=\"min-w-0 rounded-xl border bg-card text-card-foreground @md:order-2\">\n        <CrossfadeSlides slides={slides} deck={deck} />\n      </div>\n      <ol data-slot=\"card-deck-outline\" className=\"grid min-w-0 content-start gap-1 @md:order-1\">\n        {slides.map((slide, position) => (\n          <li key={slide.id} className=\"min-w-0\">\n            <button\n              type=\"button\"\n              aria-current={position === deck.active ? \"true\" : undefined}\n              onClick={() => deck.go(position)}\n              className={cn(\n                \"flex min-h-10 w-full min-w-0 items-baseline gap-2 rounded-md px-2 py-2 text-start text-[13px] text-muted-foreground outline-none\",\n                \"transition-colors duration-(--duration-fast) ease-(--ease-mise) hover:text-foreground\",\n                \"focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-current:bg-muted aria-current:font-medium aria-current:text-foreground\",\n              )}\n            >\n              <span aria-hidden=\"true\" className=\"tabular-nums\">{position + 1}</span>\n              <span className=\"min-w-0\">{slide.title}</span>\n            </button>\n          </li>\n        ))}\n      </ol>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/card-deck-state.ts",
      "content": "import type * as React from \"react\"\nimport { useEffect, useRef, useState } from \"react\"\nimport { animate, useMotionValue, type Transition } from \"motion/react\"\nimport { projectMomentum, SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\n\nconst REWIND_TOTAL_MS = 900\nconst REWIND_MAX_STEP_MS = 120\nconst SWIPE_SLOP_PX = 6\nconst INSTANT: Transition = { duration: 0 }\n\nexport function clampIndex(next: number, count: number) {\n  return Math.max(0, Math.min(Math.max(count - 1, 0), next))\n}\n\n/** Where a released swipe lands. The flick is projected first, so a short fast swipe still turns the slide. */\nexport function swipeTarget(index: number, offset: number, velocity: number, width: number, count: number) {\n  if (width <= 0) return index\n  const travel = offset + projectMomentum(velocity)\n  if (travel < -width / 2) return clampIndex(index + 1, count)\n  if (travel > width / 2) return clampIndex(index - 1, count)\n  return index\n}\n\n/** A long way back stays under a second, a short one stays readable. */\nexport function rewindStepMs(distance: number) {\n  if (distance <= 0) return 0\n  return Math.min(REWIND_MAX_STEP_MS, REWIND_TOTAL_MS / distance)\n}\n\ninterface DeckOptions {\n  count: number\n  index?: number\n  defaultIndex: number\n  onIndexChange?: (index: number) => void\n  rewindable: boolean\n  reduceMotion: boolean\n}\n\nexport function useDeckState({ count, index, defaultIndex, onIndexChange, rewindable, reduceMotion }: DeckOptions) {\n  const [uncontrolled, setUncontrolled] = useState(defaultIndex)\n  const [rewindStep, setRewindStep] = useState<number | null>(null)\n  const timer = useRef<ReturnType<typeof setTimeout>>(undefined)\n  const active = clampIndex(index ?? uncontrolled, count)\n  const last = Math.max(count - 1, 0)\n\n  useEffect(() => () => clearTimeout(timer.current), [])\n\n  const commit = (next: number) => {\n    setUncontrolled(next)\n    onIndexChange?.(next)\n  }\n\n  /** Any direct move interrupts a rewind, so the reader is never locked out while it plays. */\n  const go = (next: number) => {\n    clearTimeout(timer.current)\n    setRewindStep(null)\n    const target = clampIndex(next, count)\n    if (target !== active) commit(target)\n  }\n\n  const rewind = () => {\n    if (active === 0) return\n    if (reduceMotion || !rewindable) {\n      go(0)\n      return\n    }\n    const step = rewindStepMs(active)\n    const stepTo = (at: number) => {\n      commit(at)\n      timer.current = setTimeout(() => (at === 0 ? setRewindStep(null) : stepTo(at - 1)), step)\n    }\n    clearTimeout(timer.current)\n    setRewindStep(step)\n    stepTo(active - 1)\n  }\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n    if ((event.target as HTMLElement).closest(\"input, textarea, select, [contenteditable='true']\")) return\n    const actions: Record<string, () => void> = {\n      ArrowRight: () => go(active + 1),\n      ArrowLeft: () => go(active - 1),\n      Home: rewind,\n      End: () => go(last),\n    }\n    const action = actions[event.key]\n    if (!action) return\n    event.preventDefault()\n    action()\n  }\n\n  let transition: Transition = SPRING_DEFAULT\n  if (reduceMotion) transition = INSTANT\n  else if (rewindStep !== null) transition = { duration: rewindStep / 1000, ease: \"linear\" }\n\n  return { active, last, count, go, rewind, onKeyDown, transition, rewinding: rewindStep !== null }\n}\n\nexport type DeckState = ReturnType<typeof useDeckState>\n\ninterface Gesture {\n  pointerId: number\n  startX: number\n  lastX: number\n  lastTime: number\n  velocity: number\n  dragging: boolean\n}\n\nfunction isControl(target: EventTarget) {\n  return target instanceof Element && target.closest(\"a, button, input, select, textarea, label\") !== null\n}\n\n/** The pointer is captured only after it travels, so a tap on a link inside a slide still counts as a click. */\nexport function useDeckSwipe(deck: DeckState, reduceMotion: boolean) {\n  const dragX = useMotionValue(0)\n  const gesture = useRef<Gesture | null>(null)\n\n  const finish = (event: React.PointerEvent<HTMLElement>) => {\n    const current = gesture.current\n    gesture.current = null\n    if (!current?.dragging) return\n    const width = event.currentTarget.getBoundingClientRect().width\n    deck.go(swipeTarget(deck.active, dragX.get(), current.velocity, width, deck.count))\n    if (reduceMotion) dragX.set(0)\n    else animate(dragX, 0, SPRING_DEFAULT)\n  }\n\n  const handlers = {\n    onPointerDown: (event: React.PointerEvent<HTMLElement>) => {\n      if (event.button !== 0 || isControl(event.target)) return\n      gesture.current = {\n        pointerId: event.pointerId,\n        startX: event.clientX,\n        lastX: event.clientX,\n        lastTime: event.timeStamp,\n        velocity: 0,\n        dragging: false,\n      }\n    },\n    onPointerMove: (event: React.PointerEvent<HTMLElement>) => {\n      const current = gesture.current\n      if (!current) return\n      const offset = event.clientX - current.startX\n      if (!current.dragging && Math.abs(offset) < SWIPE_SLOP_PX) return\n      if (!current.dragging) {\n        current.dragging = true\n        event.currentTarget.setPointerCapture?.(event.pointerId)\n      }\n      const elapsed = event.timeStamp - current.lastTime\n      if (elapsed > 0) current.velocity = ((event.clientX - current.lastX) / elapsed) * 1000\n      current.lastX = event.clientX\n      current.lastTime = event.timeStamp\n      dragX.set(offset)\n    },\n    onPointerUp: finish,\n    onPointerCancel: finish,\n  }\n\n  return { dragX, handlers }\n}\n\nexport type DeckSwipe = ReturnType<typeof useDeckSwipe>\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}