{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "bs-datetime-picker", "title": "Bikram Sambat Date Time Picker (Native)", "description": "Combined BS date and time picker (iOS sheet, Android calendar then time dialog).", "dependencies": [ "clsx", "tailwind-merge", "lucide-react-native", "react-native-safe-area-context", "react-native-svg" ], "registryDependencies": [ "https://reactnativereusables.com/r/nativewind/button.json", "https://reactnativereusables.com/r/nativewind/icon.json" ], "files": [ { "path": "registry/native/files/lib/bs-datetime-picker.ts", "content": "import { clampBsDate, getDefaultBsDate } from \"./bs-picker\";\nimport { clampBsTime, getDefaultBsTime } from \"./bs-time-picker\";\nimport {\n BS_DATETIME_DISPLAY_PATTERN,\n BS_TIME_DISPLAY_PATTERN,\n formatBsDateTimePattern,\n formatBsTimePattern,\n} from \"./bs-time-picker/time/pattern\";\nimport {\n clampBsDateTime,\n formatBsDateTime,\n fromAdDate,\n getDefaultBsDateTime,\n mergeBsDateTime,\n splitBsDateTime,\n toAdDate,\n} from \"./bs-time-picker/time/datetime\";\n\nexport {\n BS_DATETIME_DISPLAY_PATTERN,\n BS_TIME_DISPLAY_PATTERN,\n clampBsDateTime,\n formatBsDateTime,\n formatBsDateTimePattern,\n formatBsTimePattern,\n fromAdDate,\n getDefaultBsDateTime,\n mergeBsDateTime,\n splitBsDateTime,\n toAdDate,\n getDefaultBsDate,\n getDefaultBsTime,\n clampBsDate,\n clampBsTime,\n};\n", "type": "registry:lib", "target": "lib/bs-datetime-picker.ts" }, { "path": "registry/native/files/components/ui/bs-time-picker-dialog.android.tsx", "content": "import {\n clampBsTime,\n formatMinuteOption,\n formatPeriodOption,\n formatTimeDigit,\n resolveDisplayHour,\n resolveDisplayPeriod,\n to12Hour,\n from12Hour,\n} from '../../lib/bs-time-picker'\nimport type { BsLocale, BsPeriod, BsTime } from '@/lib/bs-time-picker/time/types'\nimport { cn } from '@/lib/utils'\nimport * as React from 'react'\nimport { Modal, Pressable, StyleSheet, View } from 'react-native'\nimport Animated, {\n Easing,\n useAnimatedStyle,\n useSharedValue,\n withTiming,\n} from 'react-native-reanimated'\nimport Svg, { Circle, Line } from 'react-native-svg'\nimport { Text } from '@/components/ui/text'\n\nconst CLOCK_SIZE = 256\nconst CLOCK_RADIUS = 98\nconst NODE_SIZE = 40\nconst CENTER = CLOCK_SIZE / 2\n/** Reach the center of dial labels (Material hand length). */\nconst HAND_RADIUS = CLOCK_RADIUS - NODE_SIZE / 2\nconst HAND_COLOR = 'hsl(180, 82%, 24%)'\nconst DIAL_TRANSITION_MS = 320\n\ntype SelectionMode = 'hour' | 'minute'\n\ntype BsTimePickerDialogProps = {\n visible: boolean\n value: BsTime\n locale: BsLocale\n is24Hour?: boolean\n title?: string\n cancelLabel: string\n confirmLabel: string\n onChange: (value: BsTime) => void\n onCancel: () => void\n onConfirm: () => void\n}\n\n/** Degrees clockwise from 12 o'clock (Material clock convention). */\nfunction hour12ToClockDegrees(hour12: number): number {\n const normalized = hour12 === 12 ? 0 : hour12\n return normalized * 30\n}\n\nfunction hour24ToClockDegrees(hour24: number): number {\n return (hour24 / 24) * 360\n}\n\nfunction minuteToClockDegrees(minute: number): number {\n return (minute / 60) * 360\n}\n\nfunction positionForClockDegrees(clockDegrees: number, radius: number) {\n const radians = (clockDegrees - 90) * (Math.PI / 180)\n return {\n left: CENTER + radius * Math.cos(radians) - NODE_SIZE / 2,\n top: CENTER + radius * Math.sin(radians) - NODE_SIZE / 2,\n }\n}\n\nfunction getHourDialValues(is24Hour: boolean): number[] {\n return is24Hour\n ? Array.from({ length: 24 }, (_, index) => index)\n : Array.from({ length: 12 }, (_, index) => index + 1)\n}\n\nfunction getMinuteDialValues(): number[] {\n return Array.from({ length: 12 }, (_, index) => index * 5)\n}\n\nfunction isMinuteDialSelected(dialValue: number, selectedMinute: number): boolean {\n return (\n Math.abs(dialValue - selectedMinute) < 3 ||\n (selectedMinute >= 58 && dialValue === 0)\n )\n}\n\nfunction resolveSelectedMinuteDial(minute: number): number {\n return (\n getMinuteDialValues().find((dialValue) =>\n isMinuteDialSelected(dialValue, minute),\n ) ?? 0\n )\n}\n\nfunction hourValueToClockDegrees(hour: number, is24Hour: boolean): number {\n return is24Hour ? hour24ToClockDegrees(hour) : hour12ToClockDegrees(hour)\n}\n\nfunction minuteValueToClockDegrees(minute: number): number {\n return minuteToClockDegrees(minute)\n}\n\nfunction clockHandEnd(clockDegrees: number, radius: number) {\n const radians = (clockDegrees - 90) * (Math.PI / 180)\n return {\n x: CENTER + radius * Math.cos(radians),\n y: CENTER + radius * Math.sin(radians),\n }\n}\n\ntype BsTimePickerClockProps = {\n mode: SelectionMode\n value: BsTime\n locale: BsLocale\n is24Hour: boolean\n onSelectHour: (hour: number) => void\n onSelectMinute: (minute: number) => void\n}\n\nfunction ClockHand({ clockDegrees }: { clockDegrees: number }) {\n const end = clockHandEnd(clockDegrees, HAND_RADIUS)\n\n return (\n \n \n \n \n )\n}\n\ntype ClockDialNodesProps = {\n mode: SelectionMode\n value: BsTime\n locale: BsLocale\n is24Hour: boolean\n onSelectHour: (hour: number) => void\n onSelectMinute: (minute: number) => void\n}\n\nfunction ClockDialNodes({\n mode,\n value,\n locale,\n is24Hour,\n onSelectHour,\n onSelectMinute,\n}: ClockDialNodesProps) {\n const dialValues =\n mode === 'hour' ? getHourDialValues(is24Hour) : getMinuteDialValues()\n\n const selectedHour = is24Hour ? value.hour : resolveDisplayHour(value.hour, false)\n const selectedMinute = value.minute\n\n return (\n <>\n {dialValues.map((dialValue) => {\n const clockDegrees =\n mode === 'hour'\n ? hourValueToClockDegrees(dialValue, is24Hour)\n : minuteValueToClockDegrees(dialValue)\n\n const isSelected =\n mode === 'hour'\n ? dialValue === selectedHour\n : isMinuteDialSelected(dialValue, selectedMinute)\n\n const position = positionForClockDegrees(clockDegrees, CLOCK_RADIUS)\n const label =\n mode === 'hour'\n ? formatTimeDigit(dialValue, locale)\n : formatMinuteOption(dialValue, locale)\n\n return (\n {\n if (mode === 'hour') {\n if (is24Hour) {\n onSelectHour(dialValue)\n } else {\n const period = resolveDisplayPeriod(value.hour, false)\n onSelectHour(from12Hour(dialValue, period))\n }\n } else {\n onSelectMinute(dialValue)\n }\n }}\n >\n \n {label}\n \n \n )\n })}\n \n )\n}\n\ntype ClockDialLayerProps = {\n mode: SelectionMode\n value: BsTime\n locale: BsLocale\n is24Hour: boolean\n handClockDegrees: number\n onSelectHour: (hour: number) => void\n onSelectMinute: (minute: number) => void\n}\n\nfunction ClockDialLayer({\n mode,\n value,\n locale,\n is24Hour,\n handClockDegrees,\n onSelectHour,\n onSelectMinute,\n}: ClockDialLayerProps) {\n return (\n <>\n \n \n \n )\n}\n\nfunction BsTimePickerClock({\n mode,\n value,\n locale,\n is24Hour,\n onSelectHour,\n onSelectMinute,\n}: BsTimePickerClockProps) {\n const selectedHour = is24Hour ? value.hour : resolveDisplayHour(value.hour, false)\n const selectedMinute = value.minute\n const dialProgress = useSharedValue(mode === 'hour' ? 0 : 1)\n const hasAnimatedDial = React.useRef(false)\n\n React.useEffect(() => {\n const target = mode === 'hour' ? 0 : 1\n if (!hasAnimatedDial.current) {\n dialProgress.value = target\n hasAnimatedDial.current = true\n return\n }\n\n dialProgress.value = withTiming(target, {\n duration: DIAL_TRANSITION_MS,\n easing: Easing.inOut(Easing.cubic),\n })\n }, [dialProgress, mode])\n\n const hourDialStyle = useAnimatedStyle(() => ({\n opacity: 1 - dialProgress.value,\n transform: [{ scale: 1 - dialProgress.value * 0.06 }],\n }))\n\n const minuteDialStyle = useAnimatedStyle(() => ({\n opacity: dialProgress.value,\n transform: [{ scale: 0.94 + dialProgress.value * 0.06 }],\n }))\n\n const hourHandDegrees = hourValueToClockDegrees(selectedHour, is24Hour)\n const minuteHandDegrees = minuteValueToClockDegrees(\n resolveSelectedMinuteDial(selectedMinute),\n )\n\n return (\n \n \n \n \n\n \n \n \n \n )\n}\n\nconst styles = StyleSheet.create({\n dialLayer: {\n ...StyleSheet.absoluteFill,\n },\n})\n\nexport function BsTimePickerDialog({\n visible,\n value,\n locale,\n is24Hour = false,\n title,\n cancelLabel,\n confirmLabel,\n onChange,\n onCancel,\n onConfirm,\n}: BsTimePickerDialogProps) {\n const [selectionMode, setSelectionMode] = React.useState('hour')\n const clamped = clampBsTime(value)\n const displayHour = resolveDisplayHour(clamped.hour, is24Hour)\n const displayPeriod = resolveDisplayPeriod(clamped.hour, is24Hour)\n\n const updateTime = (patch: Partial) => {\n onChange(clampBsTime({ ...clamped, ...patch }))\n }\n\n const setPeriod = (period: BsPeriod) => {\n const { hour: hour12 } = to12Hour(clamped.hour)\n updateTime({ hour: from12Hour(hour12, period) })\n }\n\n const handleSelectHour = (hour24: number) => {\n updateTime({ hour: hour24 })\n setSelectionMode('minute')\n }\n\n if (!visible) return null\n\n return (\n \n \n event.stopPropagation()}\n >\n {title ? (\n \n \n {title}\n \n \n ) : null}\n\n \n \n setSelectionMode('hour')}\n >\n \n {formatTimeDigit(displayHour, locale)}\n \n \n\n \n :\n \n\n setSelectionMode('minute')}\n >\n \n {formatMinuteOption(clamped.minute, locale)}\n \n \n \n\n {!is24Hour ? (\n \n setPeriod('am')}\n >\n \n {formatPeriodOption('am', locale)}\n \n \n setPeriod('pm')}\n >\n \n {formatPeriodOption('pm', locale)}\n \n \n \n ) : null}\n \n\n \n updateTime({ minute })}\n />\n \n\n \n \n \n {cancelLabel}\n \n \n \n \n {confirmLabel}\n \n \n \n \n \n \n )\n}\n", "type": "registry:component", "target": "components/ui/bs-time-picker-dialog.android.tsx" }, { "path": "registry/native/files/components/ui/bs-wheel-column.tsx", "content": "import { Text } from '@/components/ui/text'\nimport { cn } from '@/lib/utils'\nimport * as Haptics from 'expo-haptics'\nimport * as React from 'react'\nimport {\n FlatList,\n Platform,\n ScrollView,\n useColorScheme,\n type ListRenderItemInfo,\n type NativeScrollEvent,\n type NativeSyntheticEvent,\n Pressable,\n View,\n} from 'react-native'\n\nconst ITEM_HEIGHT = Platform.OS === 'ios' ? 44 : 48\n/** Room for Devanagari matras above/below the nominal line box. */\nconst WHEEL_LABEL_LINE_HEIGHT = Platform.OS === 'ios' ? 34 : 32\nconst VISIBLE_COUNT = 5\nexport const BS_WHEEL_HEIGHT = ITEM_HEIGHT * VISIBLE_COUNT\nconst EDGE_PADDING = ITEM_HEIGHT * Math.floor(VISIBLE_COUNT / 2)\n/** Cap virtual row count for looped wheels (51× repeats blew past 1k+ rows). */\nconst MAX_LOOP_VIRTUAL_ITEMS = 180\n/** Non-virtualized snap scroll for short finite lists (e.g. BS year column). */\nconst FINITE_WHEEL_MAX_ITEMS = 120\n\nfunction getAdaptiveLoopRepeats(itemCount: number): number {\n if (itemCount <= 0) return 1\n const repeats = Math.ceil(MAX_LOOP_VIRTUAL_ITEMS / itemCount)\n const oddRepeats = repeats % 2 === 0 ? repeats + 1 : repeats\n return Math.max(7, Math.min(oddRepeats, 11))\n}\n/** iOS UIDatePicker-style selection band height (44pt row). */\nexport const BS_WHEEL_SELECTION_HEIGHT = ITEM_HEIGHT\n/** Horizontal padding beyond measured column edges. */\nconst BS_WHEEL_PILL_H_PAD = 6\n\nfunction wheelItemOpacity(distance: number): number {\n if (distance <= 0) return 1\n if (distance === 1) return 0.48\n if (distance === 2) return 0.28\n return 0.14\n}\n\n/** Gap between wheel columns (native UIDatePicker is tight). */\nexport const BS_WHEEL_COLUMN_GAP = 6\n/** Horizontal inset for date picker wheels (day / month / year). */\nexport const BS_WHEEL_DATE_ROW_INSET = 20\nexport const BS_WHEEL_DATE_COL_WIDTH = 172\nexport const BS_WHEEL_YEAR_COL_WIDTH = 56\nexport const BS_WHEEL_YEAR_COL_WIDTH_NE = 72\nexport const BS_WHEEL_MONTH_COL_WIDTH = 80\nexport const BS_WHEEL_MONTH_COL_WIDTH_NE = 100\nexport const BS_WHEEL_HOUR_COL_WIDTH = 46\nexport const BS_WHEEL_MIN_COL_WIDTH = 46\nexport const BS_WHEEL_PERIOD_COL_WIDTH = 50\n\nfunction triggerWheelSelectionHaptic() {\n if (Platform.OS === 'web') return\n void Haptics.selectionAsync()\n}\n\nfunction flattenWheelChildren(children: React.ReactNode): React.ReactNode[] {\n const nodes: React.ReactNode[] = []\n React.Children.forEach(children, (child) => {\n if (child == null || child === false) return\n if (React.isValidElement<{ children?: React.ReactNode }>(child) && child.type === React.Fragment) {\n flattenWheelChildren(child.props.children).forEach((node) => nodes.push(node))\n return\n }\n nodes.push(child)\n })\n return nodes\n}\n\ntype BsWheelSelectionBandProps = {\n /** When set, pill hugs measured column group (native UIDatePicker). */\n left?: number\n width?: number\n}\n\n/** One centered pill spanning all columns (native iOS wheel — not per-column). */\nexport function BsWheelSelectionBand({\n left,\n width,\n}: BsWheelSelectionBandProps = {}) {\n const colorScheme = useColorScheme()\n const radius = BS_WHEEL_SELECTION_HEIGHT / 2\n const top = (BS_WHEEL_HEIGHT - BS_WHEEL_SELECTION_HEIGHT) / 2\n const hugContent = width != null && width > 0 && left != null\n\n return (\n \n )\n}\n\ntype BsWheelRowProps = {\n children: React.ReactNode\n className?: string\n /** Show the shared selection band (default true for multi-column rows). */\n showSelectionBand?: boolean\n /** Gap between column groups (default BS_WHEEL_COLUMN_GAP). */\n columnGap?: number\n /** Horizontal inset inside the wheel row container. */\n horizontalInset?: number\n /** `even` spreads columns with equal space; `grouped` keeps a tight centered cluster. */\n columnLayout?: 'grouped' | 'even'\n}\n\ntype BsWheelRowMeasuredProps = {\n flatChildren: React.ReactNode[]\n showSelectionBand: boolean\n columnGap: number\n columnLayout: 'grouped' | 'even'\n}\n\nfunction BsWheelRowMeasured({\n flatChildren,\n showSelectionBand,\n columnGap,\n columnLayout,\n}: BsWheelRowMeasuredProps) {\n const childLayouts = React.useRef<{ x: number; width: number }[]>([])\n const [pillBox, setPillBox] = React.useState({ left: 0, width: 0 })\n\n const updatePillBox = React.useCallback(() => {\n const layouts = childLayouts.current\n if (layouts.length < flatChildren.length) return\n if (layouts.some((layout) => layout == null || layout.width <= 0)) return\n\n const left = Math.min(...layouts.map((layout) => layout.x))\n const right = Math.max(...layouts.map((layout) => layout.x + layout.width))\n const width = right - left\n setPillBox((prev) =>\n prev.left === left && prev.width === width ? prev : { left, width },\n )\n }, [flatChildren.length])\n\n const wrappedChildren = flatChildren.map((child, index) => (\n {\n const { x, width } = event.nativeEvent.layout\n childLayouts.current[index] = { x, width }\n updatePillBox()\n }}\n >\n {child}\n \n ))\n\n const spreadColumns = columnLayout === 'even'\n\n return (\n \n {showSelectionBand && pillBox.width > 0 ? (\n \n ) : null}\n {wrappedChildren}\n \n )\n}\n\nexport function BsWheelRow({\n children,\n className,\n showSelectionBand = true,\n columnGap = BS_WHEEL_COLUMN_GAP,\n horizontalInset = 0,\n columnLayout = 'grouped',\n}: BsWheelRowProps) {\n const flatChildren = React.useMemo(\n () => flattenWheelChildren(children),\n [children],\n )\n const spreadColumns = columnLayout === 'even'\n\n return (\n \n \n \n \n \n )\n}\n\ntype BsWheelColumnProps = {\n items: readonly T[]\n selected: T\n onSelect: (value: T) => void\n formatLabel: (value: T) => string\n className?: string\n /** Fixed column width (preferred over Tailwind width classes). */\n columnWidth?: number\n showOverlay?: boolean\n /** Tighter horizontal padding for numeric columns. */\n compact?: boolean\n /** Repeat items and recenter while scrolling for a native infinite wheel feel. */\n loop?: boolean\n loopRepeats?: number\n /** Avoid O(n) lookup when the item list is large (e.g. chronological dates). */\n selectedIndex?: number\n}\n\nfunction normalizeIndex(index: number, length: number): number {\n if (length === 0) return 0\n return ((index % length) + length) % length\n}\n\ntype BsWheelListItemProps = {\n index: number\n focusedIndex: number\n compact: boolean\n label: string\n onPressIndex: (index: number) => void\n}\n\nconst BsWheelListItem = React.memo(function BsWheelListItem({\n index,\n focusedIndex,\n compact,\n label,\n onPressIndex,\n}: BsWheelListItemProps) {\n const opacity = wheelItemOpacity(Math.abs(index - focusedIndex))\n\n return (\n onPressIndex(index)}\n >\n \n {label}\n \n \n )\n}, (prev, next) => {\n if (prev.label !== next.label || prev.index !== next.index) return false\n const prevOpacity = wheelItemOpacity(Math.abs(prev.index - prev.focusedIndex))\n const nextOpacity = wheelItemOpacity(Math.abs(next.index - next.focusedIndex))\n return prevOpacity === nextOpacity\n})\n\ntype BsWheelFiniteColumnProps = BsWheelColumnProps\n\n/** ScrollView wheel for finite lists — avoids VirtualizedList overhead (~90 years). */\nfunction BsWheelFiniteColumn({\n items,\n selected,\n onSelect,\n formatLabel,\n className,\n columnWidth,\n showOverlay = true,\n compact = false,\n selectedIndex: selectedIndexProp,\n}: BsWheelFiniteColumnProps) {\n const scrollRef = React.useRef(null)\n const selectedIndex = Math.max(\n 0,\n selectedIndexProp ?? items.indexOf(selected),\n )\n const labels = React.useMemo(\n () => items.map((item) => formatLabel(item)),\n [items, formatLabel],\n )\n const [focusedIndex, setFocusedIndex] = React.useState(selectedIndex)\n const focusedIndexRef = React.useRef(selectedIndex)\n const isSyncingRef = React.useRef(false)\n\n React.useLayoutEffect(() => {\n if (selectedIndex < 0 || selectedIndex >= items.length) return\n isSyncingRef.current = true\n focusedIndexRef.current = selectedIndex\n scrollRef.current?.scrollTo({\n y: selectedIndex * ITEM_HEIGHT,\n animated: false,\n })\n requestAnimationFrame(() => {\n isSyncingRef.current = false\n setFocusedIndex(selectedIndex)\n })\n }, [items.length, selectedIndex])\n\n const commitIndex = React.useCallback(\n (offsetY: number) => {\n const index = Math.min(\n items.length - 1,\n Math.max(0, Math.round(offsetY / ITEM_HEIGHT)),\n )\n focusedIndexRef.current = index\n setFocusedIndex(index)\n const next = items[index]\n if (next !== undefined && next !== selected) {\n onSelect(next)\n }\n },\n [items, onSelect, selected],\n )\n\n const handleScrollEnd = React.useCallback(\n (offsetY: number) => {\n triggerWheelSelectionHaptic()\n commitIndex(offsetY)\n },\n [commitIndex],\n )\n\n const handlePressIndex = React.useCallback(\n (index: number) => {\n triggerWheelSelectionHaptic()\n scrollRef.current?.scrollTo({ y: index * ITEM_HEIGHT, animated: true })\n focusedIndexRef.current = index\n setFocusedIndex(index)\n const next = items[index]\n if (next !== undefined) {\n onSelect(next)\n }\n },\n [items, onSelect],\n )\n\n return (\n \n {showOverlay ? : null}\n {\n if (isSyncingRef.current) return\n handleScrollEnd(event.nativeEvent.contentOffset.y)\n }}\n onScrollEndDrag={(event) => {\n if (isSyncingRef.current) return\n if (event.nativeEvent.velocity?.y === 0) {\n handleScrollEnd(event.nativeEvent.contentOffset.y)\n }\n }}\n >\n {items.map((item, index) => (\n \n ))}\n \n \n )\n}\n\nexport function BsWheelColumn(props: BsWheelColumnProps) {\n const useFiniteScroll =\n !props.loop &&\n props.items.length > 0 &&\n props.items.length <= FINITE_WHEEL_MAX_ITEMS\n\n if (useFiniteScroll) {\n return \n }\n\n return \n}\n\nfunction BsWheelVirtualColumn({\n items,\n selected,\n onSelect,\n formatLabel,\n className,\n columnWidth,\n showOverlay = true,\n compact = false,\n loop = false,\n loopRepeats: loopRepeatsProp,\n selectedIndex: selectedIndexProp,\n}: BsWheelColumnProps) {\n const listRef = React.useRef>(null)\n const isRecenteringRef = React.useRef(false)\n const focusedIndexRef = React.useRef(0)\n const focusFrameRef = React.useRef(null)\n const effectiveLoopRepeats = React.useMemo(\n () =>\n loop\n ? (loopRepeatsProp ?? getAdaptiveLoopRepeats(items.length))\n : 1,\n [items.length, loop, loopRepeatsProp],\n )\n const selectedIndex = Math.max(\n 0,\n selectedIndexProp ?? items.indexOf(selected),\n )\n const middleOffset =\n loop && items.length > 0\n ? Math.floor(effectiveLoopRepeats / 2) * items.length\n : 0\n\n const itemLabels = React.useMemo(\n () => items.map((item) => formatLabel(item)),\n [items, formatLabel],\n )\n\n const data = React.useMemo(() => {\n if (!loop || items.length === 0) return [...items]\n return Array.from(\n { length: items.length * effectiveLoopRepeats },\n (_, index) => items[normalizeIndex(index, items.length)]!,\n )\n }, [items, loop, effectiveLoopRepeats])\n\n const indexForSelected = React.useCallback(\n (index: number) => (loop ? middleOffset + index : index),\n [loop, middleOffset],\n )\n\n const [focusedIndex, setFocusedIndex] = React.useState(() =>\n indexForSelected(selectedIndex),\n )\n\n React.useLayoutEffect(() => {\n return () => {\n if (focusFrameRef.current != null) {\n cancelAnimationFrame(focusFrameRef.current)\n }\n }\n }, [])\n\n const scheduleFocusedIndex = React.useCallback((index: number) => {\n focusedIndexRef.current = index\n if (focusFrameRef.current != null) {\n cancelAnimationFrame(focusFrameRef.current)\n }\n focusFrameRef.current = requestAnimationFrame(() => {\n focusFrameRef.current = null\n setFocusedIndex(index)\n })\n }, [])\n\n const scrollToIndex = React.useCallback(\n (index: number, animated = false) => {\n if (index < 0 || index >= data.length) return\n listRef.current?.scrollToOffset({\n offset: index * ITEM_HEIGHT,\n animated,\n })\n scheduleFocusedIndex(index)\n },\n [data.length, scheduleFocusedIndex],\n )\n\n const recenterIfNeeded = React.useCallback(\n (index: number) => {\n if (!loop || items.length === 0) return index\n\n const edgeBuffer = items.length * 2\n if (index >= edgeBuffer && index < data.length - edgeBuffer) {\n return index\n }\n\n const normalizedIndex = normalizeIndex(index, items.length)\n const targetIndex = middleOffset + normalizedIndex\n isRecenteringRef.current = true\n listRef.current?.scrollToOffset({\n offset: targetIndex * ITEM_HEIGHT,\n animated: false,\n })\n scheduleFocusedIndex(targetIndex)\n requestAnimationFrame(() => {\n isRecenteringRef.current = false\n })\n return targetIndex\n },\n [data.length, items.length, loop, middleOffset, scheduleFocusedIndex],\n )\n\n React.useLayoutEffect(() => {\n const targetIndex = indexForSelected(selectedIndex)\n if (targetIndex < 0 || targetIndex >= data.length) return\n focusedIndexRef.current = targetIndex\n listRef.current?.scrollToOffset({\n offset: targetIndex * ITEM_HEIGHT,\n animated: false,\n })\n scheduleFocusedIndex(targetIndex)\n }, [data.length, indexForSelected, scheduleFocusedIndex, selectedIndex])\n\n const commitIndex = React.useCallback(\n (index: number) => {\n const boundedIndex = Math.min(\n data.length - 1,\n Math.max(0, Math.round(index)),\n )\n const settledIndex = recenterIfNeeded(boundedIndex)\n const normalizedIndex = loop\n ? normalizeIndex(settledIndex, items.length)\n : settledIndex\n const next = items[normalizedIndex]\n if (next !== undefined && next !== selected) {\n onSelect(next)\n }\n },\n [data.length, items, loop, onSelect, recenterIfNeeded, selected],\n )\n\n const handleScroll = (event: NativeSyntheticEvent) => {\n if (isRecenteringRef.current) return\n\n const index = Math.min(\n data.length - 1,\n Math.max(0, Math.round(event.nativeEvent.contentOffset.y / ITEM_HEIGHT)),\n )\n\n focusedIndexRef.current = index\n }\n\n const handlePressIndex = React.useCallback(\n (index: number) => {\n triggerWheelSelectionHaptic()\n scrollToIndex(index, true)\n const normalizedIndex = loop\n ? normalizeIndex(index, items.length)\n : index\n const next = items[normalizedIndex]\n if (next !== undefined) {\n onSelect(next)\n }\n },\n [items, loop, onSelect, scrollToIndex],\n )\n\n const renderItem = React.useCallback(\n ({ item, index }: ListRenderItemInfo) => {\n const labelIndex = loop\n ? normalizeIndex(index, items.length)\n : index\n return (\n \n )\n },\n [compact, focusedIndex, formatLabel, handlePressIndex, itemLabels, items.length, loop],\n )\n\n const handleMomentumEnd = (offsetY: number) => {\n const index = Math.min(\n data.length - 1,\n Math.max(0, Math.round(offsetY / ITEM_HEIGHT)),\n )\n if (index !== focusedIndex) {\n triggerWheelSelectionHaptic()\n scheduleFocusedIndex(index)\n }\n commitIndex(offsetY / ITEM_HEIGHT)\n }\n\n const initialScrollIndex = React.useMemo(\n () => indexForSelected(selectedIndex),\n // Only used on mount — do not tie to scroll focus updates.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n )\n\n return (\n \n {showOverlay ? : null}\n 0\n ? Math.min(initialScrollIndex, data.length - 1)\n : undefined\n }\n keyExtractor={(item, index) => `${String(item)}-${index}`}\n renderItem={renderItem}\n showsVerticalScrollIndicator={false}\n snapToInterval={ITEM_HEIGHT}\n decelerationRate=\"fast\"\n removeClippedSubviews={false}\n initialNumToRender={VISIBLE_COUNT + 2}\n windowSize={5}\n maxToRenderPerBatch={8}\n updateCellsBatchingPeriod={50}\n getItemLayout={(_, index) => ({\n length: ITEM_HEIGHT,\n offset: ITEM_HEIGHT * index,\n index,\n })}\n contentContainerStyle={{ paddingVertical: EDGE_PADDING }}\n onScroll={handleScroll}\n scrollEventThrottle={16}\n onMomentumScrollEnd={(event) =>\n handleMomentumEnd(event.nativeEvent.contentOffset.y)\n }\n onScrollEndDrag={(event) => {\n if (event.nativeEvent.velocity?.y === 0) {\n handleMomentumEnd(event.nativeEvent.contentOffset.y)\n }\n }}\n />\n \n )\n}\n", "type": "registry:component", "target": "components/ui/bs-wheel-column.tsx" }, { "path": "registry/native/files/components/ui/bs-wheel-sheet.tsx", "content": "import { Text } from '@/components/ui/text'\nimport * as React from 'react'\nimport { Pressable, View } from 'react-native'\n\ntype BsWheelSheetChromeProps = {\n cancelLabel: string\n confirmLabel: string\n onCancel: () => void\n onConfirm: () => void\n bottomInset: number\n children: React.ReactNode\n}\n\n/** iOS-style bottom sheet chrome for BS wheel pickers. */\nexport function BsWheelSheetChrome({\n cancelLabel,\n confirmLabel,\n onCancel,\n onConfirm,\n bottomInset,\n children,\n}: BsWheelSheetChromeProps) {\n return (\n \n \n \n {cancelLabel}\n \n \n \n {confirmLabel}\n \n \n \n\n {children}\n \n )\n}\n", "type": "registry:component", "target": "components/ui/bs-wheel-sheet.tsx" }, { "path": "registry/native/files/components/ui/bs-datetime-picker-wheels.ios.tsx", "content": "import {\n clampBsDate,\n formatBsDateWheelKey,\n getBsDateWheelKeys,\n parseBsDateWheelKey,\n} from '@/lib/bs-picker'\nimport { getBsDateIndex } from '@/lib/bs-day-picker/navigation'\nimport { bsDateKey } from '@/lib/bs-day-picker/types'\nimport type { BsDate, BsLocale } from '@/lib/bs-day-picker/types'\nimport {\n clampBsTime,\n formatHourOption,\n formatMinuteOption,\n formatPeriodOption,\n getHourOptions,\n getMinuteOptions,\n getPeriodOptions,\n resolveDisplayHour,\n resolveDisplayPeriod,\n resolveWheelHour,\n} from '../../lib/bs-time-picker'\nimport type { BsDateTime, BsPeriod, BsTime } from '@/lib/bs-time-picker/time/types'\nimport { mergeBsDateTime, splitBsDateTime } from '../../lib/bs-datetime-picker'\nimport {\n BS_WHEEL_DATE_COL_WIDTH,\n BS_WHEEL_HOUR_COL_WIDTH,\n BS_WHEEL_MIN_COL_WIDTH,\n BS_WHEEL_PERIOD_COL_WIDTH,\n BsWheelColumn,\n BsWheelRow,\n} from './bs-wheel-column'\n\ntype BsDateTimePickerWheelsProps = {\n value: BsDateTime\n locale: BsLocale\n is24Hour?: boolean\n onChange: (value: BsDateTime) => void\n}\n\nexport function BsDateTimePickerWheels({\n value,\n locale,\n is24Hour = false,\n onChange,\n}: BsDateTimePickerWheelsProps) {\n const { date, time } = splitBsDateTime(value)\n const dateKeys = getBsDateWheelKeys()\n const clampedDate = clampBsDate(date)\n const clampedTime = clampBsTime(time)\n const selectedKey = bsDateKey(clampedDate)\n const selectedIndex = getBsDateIndex(clampedDate)\n\n const hourOptions = getHourOptions(is24Hour)\n const minuteOptions = getMinuteOptions()\n const periodOptions = getPeriodOptions()\n const displayHour = resolveDisplayHour(clampedTime.hour, is24Hour)\n const displayPeriod = resolveDisplayPeriod(clampedTime.hour, is24Hour)\n\n const handleDateChange = (nextDate: BsDate) => {\n onChange(mergeBsDateTime(clampBsDate(nextDate), clampedTime))\n }\n\n const updateTime = (\n nextHour: number,\n nextMinute: number,\n nextPeriod: BsPeriod,\n ) => {\n const nextTime: BsTime = clampBsTime({\n hour: resolveWheelHour(nextHour, nextPeriod, is24Hour),\n minute: nextMinute,\n })\n onChange(mergeBsDateTime(clampedDate, nextTime))\n }\n\n return (\n \n = 0 ? selectedIndex : undefined}\n onSelect={(key) => {\n const next = parseBsDateWheelKey(key)\n if (next) handleDateChange(clampBsDate(next))\n }}\n formatLabel={(key) => formatBsDateWheelKey(key, locale)}\n />\n updateTime(hour, clampedTime.minute, displayPeriod)}\n formatLabel={(hour) => formatHourOption(hour, locale, is24Hour)}\n loop\n />\n updateTime(displayHour, minute, displayPeriod)}\n formatLabel={(minute) => formatMinuteOption(minute, locale)}\n loop\n />\n {!is24Hour ? (\n \n updateTime(displayHour, clampedTime.minute, period)\n }\n formatLabel={(period) => formatPeriodOption(period, locale)}\n />\n ) : null}\n \n )\n}\n", "type": "registry:component", "target": "components/ui/bs-datetime-picker-wheels.ios.tsx" }, { "path": "registry/native/files/components/ui/bs-datetime-picker.ios.tsx", "content": "import {\n formatBsDateTime,\n getDefaultBsDateTime,\n} from \"../../lib/bs-datetime-picker\";\nimport { formatBsDateTimePattern } from \"@/lib/bs-time-picker/time/pattern\";\nimport type { BsDateTime, BsLocale } from \"@/lib/bs-time-picker/time/types\";\nimport { cn } from \"@/lib/utils\";\nimport { CalendarClock } from \"lucide-react-native\";\nimport * as React from \"react\";\nimport { Modal, TouchableWithoutFeedback, View } from \"react-native\";\nimport { useSafeAreaInsets } from \"react-native-safe-area-context\";\nimport { BsDateTimePickerWheels } from \"./bs-datetime-picker-wheels.ios\";\nimport { BsWheelSheetChrome } from \"./bs-wheel-sheet\";\nimport { Button } from \"@/components/ui/button\";\nimport { Icon } from \"@/components/ui/icon\";\nimport { Text } from \"@/components/ui/text\";\n\nexport type BsDateTimePickerProps = {\n value?: BsDateTime;\n onValueChange?: (value: BsDateTime | undefined) => void;\n locale?: BsLocale;\n is24Hour?: boolean;\n placeholder?: string;\n cancelLabel?: string;\n confirmLabel?: string;\n formatPattern?: string;\n formatValue?: (\n value: BsDateTime,\n locale: BsLocale,\n is24Hour: boolean,\n ) => string;\n className?: string;\n disabled?: boolean;\n};\n\nfunction resolveDateTimeDisplayLabel(\n value: BsDateTime,\n locale: BsLocale,\n is24Hour: boolean,\n formatValue?: BsDateTimePickerProps[\"formatValue\"],\n formatPattern?: string,\n): string {\n if (formatValue) return formatValue(value, locale, is24Hour);\n if (formatPattern) {\n return formatBsDateTimePattern(value, formatPattern, locale, is24Hour);\n }\n return formatBsDateTime(value, locale, is24Hour);\n}\n\nexport function BsDateTimePicker({\n value,\n onValueChange,\n locale = \"ne\",\n is24Hour = false,\n placeholder,\n cancelLabel,\n confirmLabel,\n formatPattern,\n formatValue,\n className,\n disabled = false,\n}: BsDateTimePickerProps) {\n const insets = useSafeAreaInsets();\n const resolvedPlaceholder =\n placeholder ??\n (locale === \"ne\" ? \"मिति र समय छान्नुहोस्\" : \"Select date and time\");\n const resolvedCancel = cancelLabel ?? \"Cancel\";\n const resolvedConfirm = confirmLabel ?? \"Confirm\";\n const [showPicker, setShowPicker] = React.useState(false);\n const [tempValue, setTempValue] = React.useState(\n value ?? getDefaultBsDateTime(),\n );\n\n const handleOpen = () => {\n if (disabled) return;\n setTempValue(value ?? getDefaultBsDateTime());\n setShowPicker(true);\n };\n\n const handleCancel = () => {\n setShowPicker(false);\n setTempValue(value ?? getDefaultBsDateTime());\n };\n\n const handleConfirm = () => {\n setShowPicker(false);\n onValueChange?.(tempValue);\n };\n\n const displayLabel = value\n ? resolveDateTimeDisplayLabel(\n value,\n locale,\n is24Hour,\n formatValue,\n formatPattern,\n )\n : resolvedPlaceholder;\n\n return (\n <>\n \n \n {displayLabel}\n \n \n \n\n \n \n \n {}}>\n \n \n \n \n \n \n \n \n );\n}\n", "type": "registry:component", "target": "components/ui/bs-datetime-picker.ios.tsx" }, { "path": "registry/native/files/components/ui/bs-datetime-picker.tsx", "content": "export {\n BsDateTimePicker,\n type BsDateTimePickerProps,\n} from './bs-datetime-picker.ios'\n", "type": "registry:component", "target": "components/ui/bs-datetime-picker.tsx" }, { "path": "registry/native/files/components/ui/bs-datetime-picker.android.tsx", "content": "import {\n getDefaultBsDateTime,\n mergeBsDateTime,\n splitBsDateTime,\n formatBsDateTime,\n} from \"../../lib/bs-datetime-picker\";\nimport { formatBsDateTimePattern } from \"@/lib/bs-time-picker/time/pattern\";\nimport type { BsDateTime, BsLocale } from \"@/lib/bs-time-picker/time/types\";\nimport { cn } from \"@/lib/utils\";\nimport { CalendarClock } from \"lucide-react-native\";\nimport * as React from \"react\";\nimport { BsDatePickerDialog } from \"./bs-date-picker-dialog.android\";\nimport { BsTimePickerDialog } from \"./bs-time-picker-dialog.android\";\nimport { Button } from \"@/components/ui/button\";\nimport { Icon } from \"@/components/ui/icon\";\nimport { Text } from \"@/components/ui/text\";\n\nexport type BsDateTimePickerProps = {\n value?: BsDateTime;\n onValueChange?: (value: BsDateTime | undefined) => void;\n locale?: BsLocale;\n is24Hour?: boolean;\n placeholder?: string;\n title?: string;\n cancelLabel?: string;\n confirmLabel?: string;\n formatPattern?: string;\n formatValue?: (\n value: BsDateTime,\n locale: BsLocale,\n is24Hour: boolean,\n ) => string;\n className?: string;\n disabled?: boolean;\n};\n\nfunction resolveDateTimeDisplayLabel(\n value: BsDateTime,\n locale: BsLocale,\n is24Hour: boolean,\n formatValue?: BsDateTimePickerProps[\"formatValue\"],\n formatPattern?: string,\n): string {\n if (formatValue) return formatValue(value, locale, is24Hour);\n if (formatPattern) {\n return formatBsDateTimePattern(value, formatPattern, locale, is24Hour);\n }\n return formatBsDateTime(value, locale, is24Hour);\n}\n\nexport function BsDateTimePicker({\n value,\n onValueChange,\n locale = \"ne\",\n is24Hour = false,\n placeholder,\n title,\n cancelLabel,\n confirmLabel,\n formatPattern,\n formatValue,\n className,\n disabled = false,\n}: BsDateTimePickerProps) {\n const resolvedPlaceholder =\n placeholder ??\n (locale === \"ne\" ? \"मिति र समय छान्नुहोस्\" : \"Select date and time\");\n const resolvedTitle = title ?? resolvedPlaceholder;\n const resolvedCancel = cancelLabel ?? \"Cancel\";\n const resolvedConfirm = confirmLabel ?? \"OK\";\n const [showDatePicker, setShowDatePicker] = React.useState(false);\n const [showTimePicker, setShowTimePicker] = React.useState(false);\n const [tempValue, setTempValue] = React.useState(\n value ?? getDefaultBsDateTime(),\n );\n\n const handleOpen = () => {\n if (disabled) return;\n setTempValue(value ?? getDefaultBsDateTime());\n setShowDatePicker(true);\n };\n\n const handleDateCancel = () => {\n setShowDatePicker(false);\n setTempValue(value ?? getDefaultBsDateTime());\n };\n\n const handleDateConfirm = () => {\n setShowDatePicker(false);\n setShowTimePicker(true);\n };\n\n const handleTimeCancel = () => {\n setShowTimePicker(false);\n setTempValue(value ?? getDefaultBsDateTime());\n };\n\n const handleTimeConfirm = () => {\n setShowTimePicker(false);\n onValueChange?.(tempValue);\n };\n\n const displayLabel = value\n ? resolveDateTimeDisplayLabel(\n value,\n locale,\n is24Hour,\n formatValue,\n formatPattern,\n )\n : resolvedPlaceholder;\n\n const { date: dialogDate, time: dialogTime } = splitBsDateTime(tempValue);\n\n return (\n <>\n \n \n {displayLabel}\n \n \n \n\n {\n setTempValue((current) =>\n mergeBsDateTime(nextDate, splitBsDateTime(current).time),\n );\n }}\n onCancel={handleDateCancel}\n onConfirm={handleDateConfirm}\n />\n\n {\n setTempValue((current) =>\n mergeBsDateTime(splitBsDateTime(current).date, nextTime),\n );\n }}\n onCancel={handleTimeCancel}\n onConfirm={handleTimeConfirm}\n />\n \n );\n}\n", "type": "registry:component", "target": "components/ui/bs-datetime-picker.android.tsx" } ], "docs": "Installs RNR `button` (and `text`) + `icon` when missing. Install bs-calendar, bs-date-picker, and bs-time-picker first. Android: BS calendar dialog then `BsTimePickerDialog`.", "type": "registry:component" }