{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "swipe-to-confirm",
  "title": "Swipe to confirm",
  "description": "drag past 85% commits · `tone` primary/destructive · reset via `key`",
  "dependencies": [
    "lucide-react-native@^1.21.0",
    "react-native-gesture-handler@~2.32.0"
  ],
  "registryDependencies": [
    "@kit-native/icon",
    "@kit-native/lib-haptics",
    "@kit-native/lib-reduced-motion",
    "@kit-native/lib-utils",
    "@kit-native/text"
  ],
  "files": [
    {
      "path": "components/kit/swipe-to-confirm.tsx",
      "content": "import { Icon } from '@/components/ui/icon';\nimport { Text } from '@/components/ui/text';\nimport { haptic } from '@/lib/haptics';\nimport { cn } from '@/lib/utils';\nimport { useReducedMotion } from '@/lib/reduced-motion';\nimport { ArrowRightIcon, CheckIcon } from 'lucide-react-native';\nimport * as React from 'react';\nimport { Animated, Easing, type LayoutChangeEvent, View } from 'react-native';\nimport { Gesture, GestureDetector } from 'react-native-gesture-handler';\n\ntype Props = {\n  label: string;\n  onConfirm: () => void;\n  confirmLabel?: string;\n  tone?: 'primary' | 'destructive';\n  disabled?: boolean;\n  className?: string;\n};\n\nconst THUMB = 56;\nconst TRACK_H = 64;\nconst INSET = (TRACK_H - THUMB) / 2;\nconst COMMIT_RATIO = 0.85;\n\n/**\n * Drag-to-confirm for high-stakes actions, on a native pan gesture so the drag is not stolen\n * by swipe-back or a parent ScrollView. Past 85% commits (success haptic), otherwise springs\n * back. Does not reset itself: change its `key` to reset.\n */\nexport function SwipeToConfirm({ label, onConfirm, confirmLabel = 'Confirmed', tone = 'primary', disabled, className }: Props) {\n  const [trackW, setTrackW] = React.useState(0);\n  const [completed, setCompleted] = React.useState(false);\n  const [translateX] = React.useState(() => new Animated.Value(0));\n  const maxX = Math.max(0, trackW - THUMB - INSET * 2);\n\n  // The drag itself is the finger, which Reduce Motion does not govern — but the\n  // snap-back and the commit slide are animations, so they jump instead.\n  const reduced = useReducedMotion();\n\n  const latest = React.useRef({ maxX, completed, onConfirm, reduced });\n  React.useEffect(() => {\n    latest.current = { maxX, completed, onConfirm, reduced };\n  });\n\n  const springBack = React.useCallback(\n    (instant: boolean) => {\n      if (instant) {\n        translateX.setValue(0);\n        return;\n      }\n      Animated.spring(translateX, { toValue: 0, friction: 7, tension: 60, useNativeDriver: false }).start();\n    },\n    [translateX]\n  );\n\n  // Gesture callbacks run at event time; the ref reads inside are safe.\n  /* eslint-disable react-hooks/refs */\n  const pan = React.useMemo(\n    () =>\n      Gesture.Pan()\n        .enabled(!disabled)\n        .runOnJS(true)\n        .activeOffsetX([-6, 6])\n        .failOffsetY([-14, 14])\n        .onUpdate((e) => {\n          if (latest.current.completed) return;\n          translateX.setValue(Math.max(0, Math.min(e.translationX, latest.current.maxX)));\n        })\n        .onEnd((e) => {\n          const { maxX, completed, onConfirm, reduced } = latest.current;\n          if (completed) return;\n          const clamped = Math.max(0, Math.min(e.translationX, maxX));\n          if (maxX > 0 && clamped / maxX >= COMMIT_RATIO) {\n            haptic('success');\n            setCompleted(true);\n            Animated.timing(translateX, { toValue: maxX, duration: reduced ? 0 : 120, easing: Easing.out(Easing.cubic), useNativeDriver: false }).start(() => onConfirm());\n          } else {\n            springBack(reduced);\n          }\n        })\n        .onFinalize((_e, success) => {\n          if (!success && !latest.current.completed) springBack(latest.current.reduced);\n        }),\n    [disabled, translateX, springBack]\n  );  /* eslint-enable react-hooks/refs */\n\n\n  const promptOpacity = maxX > 0 ? translateX.interpolate({ inputRange: [0, maxX * 0.5], outputRange: [1, 0], extrapolate: 'clamp' }) : 1;\n  const fillWidth = Animated.add(translateX, new Animated.Value(THUMB + INSET * 2));\n  const bg = tone === 'destructive' ? 'bg-destructive' : 'bg-primary';\n  const fg = tone === 'destructive' ? 'text-destructive-foreground' : 'text-primary-foreground';\n\n  return (\n    <View\n      onLayout={(e: LayoutChangeEvent) => setTrackW(e.nativeEvent.layout.width)}\n      className={cn('bg-muted w-full justify-center overflow-hidden rounded-control', disabled && 'opacity-50', className)}\n      style={{ height: TRACK_H }}\n      accessibilityRole=\"button\"\n      accessibilityLabel={label}\n      accessibilityHint=\"Swipe right to confirm\">\n      <Animated.View pointerEvents=\"none\" style={{ position: 'absolute', top: 0, bottom: 0, left: 0, width: fillWidth }}>\n        <View className={cn('h-full w-full rounded-control', tone === 'destructive' ? 'bg-destructive/15' : 'bg-primary/15')} />\n      </Animated.View>\n      <Animated.View\n        pointerEvents=\"none\"\n        style={{ position: 'absolute', left: 0, right: 0, alignItems: 'center', opacity: completed ? 0 : promptOpacity }}>\n        <Text className=\"text-muted-foreground font-semibold\">{label}</Text>\n      </Animated.View>\n      {completed ? (\n        <View pointerEvents=\"none\" className=\"absolute left-0 right-0 items-center\">\n          <Text className={cn('font-semibold', tone === 'destructive' ? 'text-destructive' : 'text-primary')}>{confirmLabel}</Text>\n        </View>\n      ) : null}\n      <GestureDetector gesture={pan}>\n        <Animated.View style={{ width: THUMB, height: THUMB, marginLeft: INSET, transform: [{ translateX }] }}>\n          <View className={cn('h-full w-full items-center justify-center rounded-control', bg)}>\n            <Icon as={completed ? CheckIcon : ArrowRightIcon} size={24} className={fg} />\n          </View>\n        </Animated.View>\n      </GestureDetector>\n    </View>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/kit/swipe-to-confirm.tsx"
    }
  ],
  "meta": {
    "exports": [
      "SwipeToConfirm"
    ],
    "api": "<SwipeToConfirm label onConfirm confirmLabel? tone=\"primary|destructive\" />"
  },
  "docs": "Run `npx expo install --fix` afterwards so Expo SDK packages match your SDK. Needs the kit theme (`@kit-native/theme`) and `KitThemeProvider` from `@kit-native/lib-theme-context` at the root.",
  "categories": [
    "actions"
  ],
  "type": "registry:component"
}