import React, { useCallback, useEffect, useRef, useState } from "react";
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
import * as Haptics from "expo-haptics";
import { useColorScheme } from "nativewind";
import { Delete, Fingerprint } from "lucide-react-native";
const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
/** Key diameter bounds, and the gap between them. */
const MIN_KEY = 76;
const MAX_KEY = 104;
const GAP = 16;
/** Assumed width for the single frame before the first measurement lands. */
const ASSUMED_WIDTH = 320;
export function PinPad({ length = 6, onComplete, onChange, onBiometric, resetKey, error, haptics = true, }) {
    const { colorScheme } = useColorScheme();
    const isDark = colorScheme === "dark";
    const iconColor = isDark ? "#FFFFFF" : "#000000";
    const [available, setAvailable] = useState(0);
    const onMeasure = (event) => {
        const measured = event.nativeEvent.layout.width;
        if (measured > 0 && Math.abs(measured - available) > 1) {
            setAvailable(measured);
        }
    };
    // Three keys and two gaps have to fit in the width this pad was actually
    // given. Clamped at both ends: MIN_KEY keeps the pad usable in a narrow
    // column even if that means it overflows a little, MAX_KEY stops a tablet
    // from rendering three dinner plates.
    const width = available || ASSUMED_WIDTH;
    const keySize = Math.max(MIN_KEY, Math.min(MAX_KEY, Math.floor((width - 2 * GAP) / 3)));
    const padWidth = keySize * 3 + GAP * 2;
    // Dots at 0.22 of a key rather than 0.17. They are the only readout of how
    // much of the PIN has landed, and at the smaller ratio they were a row of
    // specks beneath a keypad that dwarfed them.
    const dotSize = Math.round(keySize * 0.22);
    const [pin, setPin] = useState("");
    // Source of truth for the latest digits — lets callbacks fire OUTSIDE the
    // setPin updater (calling a parent setState inside an updater triggers
    // "setState while rendering"). Kept in sync with state on every render.
    const pinRef = useRef(pin);
    pinRef.current = pin;
    // Clear when resetKey changes.
    const [seenKey, setSeenKey] = useState(resetKey);
    if (resetKey !== seenKey) {
        setSeenKey(resetKey);
        if (pin) {
            pinRef.current = "";
            setPin("");
        }
    }
    /**
     * A tap you can feel.
     *
     * Deliberately fire-and-forget: the feedback is worth nothing if the digit
     * waits for it, and a device with no haptic engine rejects the promise rather
     * than throwing, which must not surface as an unhandled rejection over a
     * keypress.
     */
    const tap = useCallback(() => {
        if (!haptics || Platform.OS === "web") {
            return;
        }
        void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {
            // No haptic engine, or the OS declined. Nothing to say about it.
        });
    }, [haptics]);
    const press = useCallback((d) => {
        const cur = pinRef.current;
        if (cur.length >= length) {
            return;
        }
        tap();
        const next = cur + d;
        pinRef.current = next;
        setPin(next);
        onChange?.(next);
        if (next.length === length) {
            onComplete(next);
        }
    }, [length, onChange, onComplete, tap]);
    const back = useCallback(() => {
        if (pinRef.current.length === 0) {
            return;
        }
        tap();
        const next = pinRef.current.slice(0, -1);
        pinRef.current = next;
        setPin(next);
        onChange?.(next);
    }, [onChange, tap]);
    // Physical keyboard / numpad support (web): digits type, Backspace/Delete
    // removes, Enter submits a full PIN.
    useEffect(() => {
        if (Platform.OS !== "web" || typeof window === "undefined") {
            return;
        }
        const onKey = (e) => {
            if (e.key >= "0" && e.key <= "9") {
                e.preventDefault();
                press(e.key);
            }
            else if (e.key === "Backspace" || e.key === "Delete") {
                e.preventDefault();
                back();
            }
            else if (e.key === "Enter") {
                e.preventDefault();
                if (pinRef.current.length === length) {
                    onComplete(pinRef.current);
                }
            }
        };
        window.addEventListener("keydown", onKey);
        return () => window.removeEventListener("keydown", onKey);
    }, [press, back, onComplete, length]);
    return (<View className="w-full items-center gap-8" onLayout={onMeasure}>
      {/* dots */}
      <View className="flex-row" style={{ gap: Math.round(dotSize * 0.9) }}>
        {Array.from({ length }).map((_, i) => (<View key={i} style={{ width: dotSize, height: dotSize, borderRadius: dotSize / 2 }} className={i < pin.length
                ? error
                    ? "bg-destructive dark:bg-red-900"
                    : "bg-primary dark:bg-white"
                : "border-2 border-border dark:border-neutral-700"}/>))}
      </View>

      {/* keypad */}
      <View className="flex-row flex-wrap justify-center" style={{ width: padWidth, gap: GAP }}>
        {KEYS.map((k) => (<Key key={k} label={k} size={keySize} onPress={() => press(k)}/>))}
        {onBiometric ? (<Key size={keySize} onPress={onBiometric} icon={<Fingerprint size={Math.round(keySize * 0.34)} color={iconColor}/>} accessibilityLabel="Unlock with biometrics"/>) : (<View style={{ width: keySize, height: keySize }}/>)}
        <Key label="0" size={keySize} onPress={() => press("0")}/>
        <Key size={keySize} onPress={back} icon={<Delete size={Math.round(keySize * 0.3)} color={iconColor}/>} accessibilityLabel="Delete"/>
      </View>
    </View>);
}
function Key({ label, icon, size, onPress, accessibilityLabel, }) {
    // The resting key is a ring and a digit, so the press has to carry all the
    // feedback: scale dips, and the fill fades up from nothing underneath. Driven
    // from one shared value so the two cannot fall out of step.
    const pressed = useSharedValue(0);
    const pressStyle = useAnimatedStyle(() => ({
        transform: [{ scale: 1 - pressed.value * 0.08 }],
    }));
    const fillStyle = useAnimatedStyle(() => ({ opacity: pressed.value }));
    return (<Animated.View style={pressStyle}>
      <Pressable onPress={onPress} onPressIn={() => {
            pressed.value = withTiming(1, { duration: 60 });
        }} onPressOut={() => {
            pressed.value = withTiming(0, { duration: 160 });
        }} style={{ width: size, height: size, borderRadius: size / 2 }} className="items-center justify-center overflow-hidden border border-border dark:border-neutral-700" accessibilityRole="button" accessibilityLabel={accessibilityLabel ?? label ?? "key"}>
        {/* The pressed fill, behind the digit. A sibling with animated opacity
            rather than an animated backgroundColor: colour has to come from the
            theme's classes, and those cannot be interpolated. */}
        <Animated.View pointerEvents="none" style={[fillStyle, StyleSheet.absoluteFillObject]} className="bg-secondary dark:bg-neutral-800"/>
        {icon ?? (<Text className="font-mono text-foreground dark:text-white" style={{ fontSize: Math.round(size * 0.36) }}>
            {label}
          </Text>)}
      </Pressable>
    </Animated.View>);
}
export default PinPad;
//# sourceMappingURL=pin-pad.jsx.map