{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "bs-date-picker",
"title": "Bikram Sambat Date Picker (Native)",
"description": "Platform Bikram Sambat date picker for React Native / Expo (iOS wheel, Android Material calendar, web dropdowns).",
"dependencies": [
"clsx",
"tailwind-merge",
"lucide-react-native",
"react-native-safe-area-context"
],
"registryDependencies": [
"https://reactnativereusables.com/r/nativewind/button.json",
"https://reactnativereusables.com/r/nativewind/icon.json"
],
"files": [
{
"path": "registry/native/files/lib/bs-picker.ts",
"content": "import { getMonthLabel } from '@/lib/bs-day-picker/constants'\nimport {\n formatBsDateWheelLabel,\n formatDayLabel,\n formatYearLabel,\n} from '@/lib/bs-day-picker/formatters'\nimport {\n clampMonth,\n getAllBsDates,\n getCurrentBsDate,\n getMonthData,\n getYears,\n} from '@/lib/bs-day-picker/navigation'\nimport {\n bsDateKey,\n parseBsDateKey,\n type BsDate,\n type BsLocale,\n} from '@/lib/bs-day-picker/types'\n\nexport function clampBsDate(date: BsDate): BsDate {\n const month = clampMonth({ year: date.year, month: date.month })\n const monthData = getMonthData(month.year, month.month)\n const daysInMonth = monthData?.daysInMonth ?? 30\n\n return {\n year: month.year,\n month: month.month,\n day: Math.min(Math.max(1, date.day), daysInMonth),\n }\n}\n\nexport function getDefaultBsDate(): BsDate {\n return getCurrentBsDate()\n}\n\nexport function getBsDayOptions(year: number, month: number): number[] {\n const monthData = getMonthData(year, month)\n const count = monthData?.daysInMonth ?? 30\n return Array.from({ length: count }, (_, index) => index + 1)\n}\n\nexport function getBsYearOptions(): number[] {\n return getYears()\n}\n\nexport function getBsMonthOptions(locale: BsLocale, short = false) {\n return Array.from({ length: 12 }, (_, index) => {\n const month = index + 1\n return {\n value: month,\n label: getMonthLabel(month, locale, short),\n }\n })\n}\n\nexport function formatBsYearOption(year: number, locale: BsLocale): string {\n return formatYearLabel(year, locale)\n}\n\nexport function formatBsDayOption(day: number, locale: BsLocale): string {\n return formatDayLabel(day, locale)\n}\n\nlet cachedBsDateWheelKeys: string[] | null = null\n\nexport function getBsDateWheelKeys(): readonly string[] {\n if (!cachedBsDateWheelKeys) {\n cachedBsDateWheelKeys = getAllBsDates().map(bsDateKey)\n }\n return cachedBsDateWheelKeys\n}\n\nexport function formatBsDateWheelKey(key: string, locale: BsLocale): string {\n const date = parseBsDateKey(key)\n if (!date) return key\n return formatBsDateWheelLabel(date, locale)\n}\n\nexport function parseBsDateWheelKey(key: string): BsDate | null {\n return parseBsDateKey(key)\n}\n",
"type": "registry:lib",
"target": "lib/bs-picker.ts"
},
{
"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-date-picker-wheels.ios.tsx",
"content": "import {\n clampBsDate,\n formatBsDayOption,\n formatBsYearOption,\n getBsDayOptions,\n getBsMonthOptions,\n getBsYearOptions,\n} from '@/lib/bs-picker'\nimport type { BsDate, BsLocale } from '@/lib/bs-day-picker/types'\nimport * as React from 'react'\nimport {\n BS_WHEEL_DATE_ROW_INSET,\n BS_WHEEL_MIN_COL_WIDTH,\n BS_WHEEL_MONTH_COL_WIDTH,\n BS_WHEEL_MONTH_COL_WIDTH_NE,\n BS_WHEEL_YEAR_COL_WIDTH,\n BS_WHEEL_YEAR_COL_WIDTH_NE,\n BsWheelColumn,\n BsWheelRow,\n} from './bs-wheel-column'\n\ntype BsDatePickerWheelsProps = {\n value: BsDate\n locale: BsLocale\n onChange: (value: BsDate) => void\n}\n\ntype BsDatePickerDayWheelProps = {\n year: number\n month: number\n day: number\n locale: BsLocale\n onDayChange: (day: number) => void\n}\n\nconst BsDatePickerDayWheel = React.memo(function BsDatePickerDayWheel({\n year,\n month,\n day,\n locale,\n onDayChange,\n}: BsDatePickerDayWheelProps) {\n const days = React.useMemo(\n () => getBsDayOptions(year, month),\n [year, month],\n )\n const formatDay = React.useCallback(\n (value: number) => formatBsDayOption(value, locale),\n [locale],\n )\n\n return (\n \n )\n})\n\ntype BsDatePickerMonthWheelProps = {\n month: number\n locale: BsLocale\n onMonthChange: (month: number) => void\n}\n\nconst BsDatePickerMonthWheel = React.memo(function BsDatePickerMonthWheel({\n month,\n locale,\n onMonthChange,\n}: BsDatePickerMonthWheelProps) {\n const months = React.useMemo(() => getBsMonthOptions(locale), [locale])\n const monthValues = React.useMemo(\n () => months.map((entry) => entry.value),\n [months],\n )\n const monthLabels = React.useMemo(\n () => new Map(months.map((entry) => [entry.value, entry.label])),\n [months],\n )\n const formatMonth = React.useCallback(\n (value: number) => monthLabels.get(value) ?? String(value),\n [monthLabels],\n )\n\n return (\n \n )\n})\n\ntype BsDatePickerYearWheelProps = {\n year: number\n locale: BsLocale\n onYearChange: (year: number) => void\n}\n\nconst BsDatePickerYearWheel = React.memo(function BsDatePickerYearWheel({\n year,\n locale,\n onYearChange,\n}: BsDatePickerYearWheelProps) {\n const years = React.useMemo(() => getBsYearOptions(), [])\n const formatYear = React.useCallback(\n (value: number) => formatBsYearOption(value, locale),\n [locale],\n )\n\n return (\n \n )\n})\n\nexport function BsDatePickerWheels({\n value,\n locale,\n onChange,\n}: BsDatePickerWheelsProps) {\n const clamped = clampBsDate(value)\n const clampedRef = React.useRef(clamped)\n React.useEffect(() => {\n clampedRef.current = clamped\n }, [clamped])\n\n const handleDayChange = React.useCallback(\n (day: number) => {\n onChange(clampBsDate({ ...clampedRef.current, day }))\n },\n [onChange],\n )\n const handleMonthChange = React.useCallback(\n (month: number) => {\n onChange(clampBsDate({ ...clampedRef.current, month }))\n },\n [onChange],\n )\n const handleYearChange = React.useCallback(\n (year: number) => {\n onChange(clampBsDate({ ...clampedRef.current, year }))\n },\n [onChange],\n )\n\n return (\n \n \n \n \n \n )\n}\n",
"type": "registry:component",
"target": "components/ui/bs-date-picker-wheels.ios.tsx"
},
{
"path": "registry/native/files/components/ui/bs-date-picker.ios.tsx",
"content": "import { getDefaultBsDate } from '@/lib/bs-picker'\nimport {\n BS_DATE_DISPLAY_PATTERN,\n formatBsDatePattern,\n} from '@/lib/bs-day-picker/pattern'\nimport type { BsDate, BsLocale } from '@/lib/bs-day-picker/types'\nimport { cn } from '@/lib/utils'\nimport { Calendar } 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 { BsDatePickerWheels } from './bs-date-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 BsDatePickerProps = {\n value?: BsDate\n onValueChange?: (value: BsDate | undefined) => void\n locale?: BsLocale\n placeholder?: string\n title?: string\n cancelLabel?: string\n confirmLabel?: string\n formatPattern?: string\n formatValue?: (value: BsDate, locale: BsLocale) => string\n className?: string\n disabled?: boolean\n}\n\nfunction resolveDateDisplayLabel(\n value: BsDate,\n locale: BsLocale,\n formatValue?: BsDatePickerProps['formatValue'],\n formatPattern?: string,\n): string {\n if (formatValue) return formatValue(value, locale)\n return formatBsDatePattern(\n value,\n formatPattern ?? BS_DATE_DISPLAY_PATTERN,\n locale,\n )\n}\n\nexport function BsDatePicker({\n value,\n onValueChange,\n locale = 'ne',\n placeholder,\n title: _title,\n cancelLabel,\n confirmLabel,\n formatPattern,\n formatValue,\n className,\n disabled = false,\n}: BsDatePickerProps) {\n const insets = useSafeAreaInsets()\n const resolvedPlaceholder =\n placeholder ?? (locale === 'ne' ? 'मिति छान्नुहोस्' : 'Select date')\n const resolvedCancel = cancelLabel ?? 'Cancel'\n const resolvedConfirm = confirmLabel ?? 'Confirm'\n const [showPicker, setShowPicker] = React.useState(false)\n const [tempDate, setTempDate] = React.useState(\n value ?? getDefaultBsDate(),\n )\n\n const handleOpen = () => {\n if (disabled) return\n setTempDate(value ?? getDefaultBsDate())\n setShowPicker(true)\n }\n\n const handleCancel = () => {\n setShowPicker(false)\n setTempDate(value ?? getDefaultBsDate())\n }\n\n const handleConfirm = () => {\n setShowPicker(false)\n onValueChange?.(tempDate)\n }\n\n const displayLabel = value\n ? resolveDateDisplayLabel(value, locale, formatValue, formatPattern)\n : resolvedPlaceholder\n\n return (\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-date-picker.ios.tsx"
},
{
"path": "registry/native/files/components/ui/bs-date-picker.tsx",
"content": "export { BsDatePicker, type BsDatePickerProps } from './bs-date-picker.ios'\n",
"type": "registry:component",
"target": "components/ui/bs-date-picker.tsx"
},
{
"path": "registry/native/files/components/ui/bs-date-picker.android.tsx",
"content": "import { getDefaultBsDate } from '@/lib/bs-picker'\nimport {\n BS_DATE_DISPLAY_PATTERN,\n formatBsDatePattern,\n} from '@/lib/bs-day-picker/pattern'\nimport type { BsDate, BsLocale } from '@/lib/bs-day-picker/types'\nimport { cn } from '@/lib/utils'\nimport { Calendar } from 'lucide-react-native'\nimport * as React from 'react'\nimport { BsDatePickerDialog } from './bs-date-picker-dialog.android'\nimport { Button } from '@/components/ui/button'\nimport { Icon } from '@/components/ui/icon'\nimport { Text } from '@/components/ui/text'\n\nexport type BsDatePickerProps = {\n value?: BsDate\n onValueChange?: (value: BsDate | undefined) => void\n locale?: BsLocale\n placeholder?: string\n title?: string\n cancelLabel?: string\n confirmLabel?: string\n formatPattern?: string\n formatValue?: (value: BsDate, locale: BsLocale) => string\n className?: string\n disabled?: boolean\n}\n\nfunction resolveDateDisplayLabel(\n value: BsDate,\n locale: BsLocale,\n formatValue?: BsDatePickerProps['formatValue'],\n formatPattern?: string,\n): string {\n if (formatValue) return formatValue(value, locale)\n return formatBsDatePattern(\n value,\n formatPattern ?? BS_DATE_DISPLAY_PATTERN,\n locale,\n )\n}\n\nexport function BsDatePicker({\n value,\n onValueChange,\n locale = 'ne',\n placeholder,\n title,\n cancelLabel,\n confirmLabel,\n formatPattern,\n formatValue,\n className,\n disabled = false,\n}: BsDatePickerProps) {\n const resolvedPlaceholder =\n placeholder ?? (locale === 'ne' ? 'मिति छान्नुहोस्' : 'Select date')\n const resolvedTitle = title ?? resolvedPlaceholder\n const resolvedCancel = cancelLabel ?? 'Cancel'\n const resolvedConfirm = confirmLabel ?? 'OK'\n const [showPicker, setShowPicker] = React.useState(false)\n const [tempDate, setTempDate] = React.useState(\n value ?? getDefaultBsDate(),\n )\n\n const handleOpen = () => {\n if (disabled) return\n setTempDate(value ?? getDefaultBsDate())\n setShowPicker(true)\n }\n\n const handleCancel = () => {\n setShowPicker(false)\n setTempDate(value ?? getDefaultBsDate())\n }\n\n const handleConfirm = () => {\n setShowPicker(false)\n onValueChange?.(tempDate)\n }\n\n const displayLabel = value\n ? resolveDateDisplayLabel(value, locale, formatValue, formatPattern)\n : resolvedPlaceholder\n\n return (\n <>\n \n\n \n >\n )\n}\n",
"type": "registry:component",
"target": "components/ui/bs-date-picker.android.tsx"
},
{
"path": "registry/native/files/components/ui/bs-date-picker-dialog.android.tsx",
"content": "import { BsCalendar } from '@/components/ui/bs-calendar'\nimport { formatBsDateHeadline } from '@/lib/bs-day-picker/formatters'\nimport type { BsDate, BsLocale } from '@/lib/bs-day-picker/types'\nimport { Modal, Pressable, View } from 'react-native'\nimport { Text } from '@/components/ui/text'\n\n/** Dialog calendar body — fits nav + weekdays + 6 week rows */\nconst BS_MATERIAL_CALENDAR_BODY_HEIGHT = 320\n\ntype BsDatePickerDialogProps = {\n visible: boolean\n value: BsDate\n locale: BsLocale\n title: string\n cancelLabel: string\n confirmLabel: string\n onChange: (value: BsDate) => void\n onCancel: () => void\n onConfirm: () => void\n}\n\nexport function BsDatePickerDialog({\n visible,\n value,\n locale,\n title,\n cancelLabel,\n confirmLabel,\n onChange,\n onCancel,\n onConfirm,\n}: BsDatePickerDialogProps) {\n if (!visible) return null\n\n return (\n \n \n event.stopPropagation()}\n >\n \n \n {title}\n \n \n {formatBsDateHeadline(value, locale)}\n \n \n\n \n {\n if (date) onChange(date)\n }}\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-date-picker-dialog.android.tsx"
}
],
"docs": "Installs RNR `button` (and `text`) + `icon` when missing. Install bs-calendar first. iOS: sheet + wheels (`.ios.tsx`). Android: Material calendar dialog.",
"type": "registry:component"
}