{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "bs-time-picker",
"title": "Bikram Sambat Time Picker (Native)",
"description": "Platform Bikram Sambat time picker (iOS wheels, Android Material 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-time-picker.ts",
"content": "import {\n clampBsTime,\n formatHourWheelLabel,\n formatMinuteWheelLabel,\n formatPeriodLabel,\n formatBsTime,\n formatTimeDigit,\n from12Hour,\n getDefaultBsTime,\n getHourOptions,\n getMinuteOptions,\n getPeriodOptions,\n to12Hour,\n} from '@/lib/bs-time-picker/time'\nimport type { BsLocale, BsPeriod, BsTime } from '@/lib/bs-time-picker/time/types'\n\nexport {\n clampBsTime,\n formatTimeDigit,\n getDefaultBsTime,\n getHourOptions,\n getMinuteOptions,\n getPeriodOptions,\n to12Hour,\n from12Hour,\n}\n\nexport function formatBsTimeWheelLabel(\n time: BsTime,\n locale: BsLocale,\n is24Hour: boolean,\n): string {\n return formatBsTime(time, locale, is24Hour)\n}\n\nexport function formatHourOption(\n hour: number,\n locale: BsLocale,\n is24Hour: boolean,\n): string {\n return formatHourWheelLabel(hour, locale, is24Hour)\n}\n\nexport function formatMinuteOption(minute: number, locale: BsLocale): string {\n return formatMinuteWheelLabel(minute, locale)\n}\n\nexport function formatPeriodOption(period: BsPeriod, locale: BsLocale): string {\n return formatPeriodLabel(period, locale)\n}\n\nexport function resolveWheelHour(\n displayHour: number,\n period: BsPeriod,\n is24Hour: boolean,\n): number {\n if (is24Hour) return displayHour\n return from12Hour(displayHour, period)\n}\n\nexport function resolveDisplayHour(hour24: number, is24Hour: boolean): number {\n if (is24Hour) return hour24\n return to12Hour(hour24).hour\n}\n\nexport function resolveDisplayPeriod(hour24: number, is24Hour: boolean): BsPeriod {\n if (is24Hour) return 'am'\n return to12Hour(hour24).period\n}\n",
"type": "registry:lib",
"target": "lib/bs-time-picker.ts"
},
{
"path": "registry/native/files/lib/bs-time-picker/time/types.ts",
"content": "import type { BsDate, BsLocale } from '../../bs-day-picker/types'\n\nexport type { BsLocale }\n\nexport type BsTime = {\n hour: number\n minute: number\n}\n\nexport type BsDateTime = BsDate & BsTime\n\nexport type BsPeriod = 'am' | 'pm'\n\nexport function bsTimeKey(time: BsTime): string {\n return `${time.hour}:${time.minute}`\n}\n\nexport function parseBsTimeKey(key: string): BsTime | null {\n const match = key.match(/^(\\d+):(\\d+)$/)\n if (!match) return null\n return {\n hour: Number(match[1]),\n minute: Number(match[2]),\n }\n}\n\nexport function bsTimesEqual(a?: BsTime, b?: BsTime): boolean {\n if (!a || !b) return false\n return a.hour === b.hour && a.minute === b.minute\n}\n",
"type": "registry:lib",
"target": "lib/bs-time-picker/time/types.ts"
},
{
"path": "registry/native/files/lib/bs-time-picker/time/helpers.ts",
"content": "import type { BsPeriod, BsTime } from './types'\n\nexport function clampBsTime(time: BsTime): BsTime {\n const hour = Math.min(23, Math.max(0, Math.round(time.hour)))\n const minute = Math.min(59, Math.max(0, Math.round(time.minute)))\n return { hour, minute }\n}\n\nexport function getDefaultBsTime(): BsTime {\n const now = new Date()\n return { hour: now.getHours(), minute: now.getMinutes() }\n}\n\nexport function getHourOptions(is24Hour: boolean): number[] {\n if (is24Hour) {\n return Array.from({ length: 24 }, (_, index) => index)\n }\n return Array.from({ length: 12 }, (_, index) => index + 1)\n}\n\nexport function getMinuteOptions(): number[] {\n return Array.from({ length: 60 }, (_, index) => index)\n}\n\nexport function getPeriodOptions(): BsPeriod[] {\n return ['am', 'pm']\n}\n\nexport function to12Hour(hour24: number): { hour: number; period: BsPeriod } {\n const normalized = ((hour24 % 24) + 24) % 24\n const period: BsPeriod = normalized >= 12 ? 'pm' : 'am'\n const hour12 = normalized % 12 || 12\n return { hour: hour12, period }\n}\n\nexport function from12Hour(hour12: number, period: BsPeriod): number {\n const normalized = ((hour12 % 12) + 12) % 12 || 12\n if (period === 'am') {\n return normalized === 12 ? 0 : normalized\n }\n return normalized === 12 ? 12 : normalized + 12\n}\n\nexport function bsTimeToDate(time: BsTime, base = new Date()): Date {\n const date = new Date(base)\n date.setHours(time.hour, time.minute, 0, 0)\n return date\n}\n\nexport function dateToBsTime(date: Date): BsTime {\n return {\n hour: date.getHours(),\n minute: date.getMinutes(),\n }\n}\n",
"type": "registry:lib",
"target": "lib/bs-time-picker/time/helpers.ts"
},
{
"path": "registry/native/files/lib/bs-time-picker/time/formatters.ts",
"content": "import { pad2 } from '../../bs-day-picker/constants'\nimport type { BsLocale } from '../../bs-day-picker/types'\nimport { to12Hour } from './helpers'\nimport type { BsTime } from './types'\n\nexport function formatTimeDigit(value: number, locale: BsLocale): string {\n return pad2(value, locale)\n}\n\nexport function formatBsTime(\n time: BsTime,\n locale: BsLocale = 'en',\n is24Hour = false,\n): string {\n if (is24Hour) {\n return `${formatTimeDigit(time.hour, locale)}:${formatTimeDigit(time.minute, locale)}`\n }\n\n const { hour, period } = to12Hour(time.hour)\n const periodLabel =\n locale === 'ne'\n ? period === 'am'\n ? 'पूर्वाह्न'\n : 'अपराह्न'\n : period.toUpperCase()\n return `${formatTimeDigit(hour, locale)}:${formatTimeDigit(time.minute, locale)} ${periodLabel}`\n}\n\nexport function formatBsTimeWheelLabel(\n time: BsTime,\n locale: BsLocale,\n is24Hour: boolean,\n): string {\n return formatBsTime(time, locale, is24Hour)\n}\n\nexport function formatHourWheelLabel(\n hour: number,\n locale: BsLocale,\n is24Hour: boolean,\n): string {\n if (is24Hour) return formatTimeDigit(hour, locale)\n return formatTimeDigit(hour, locale)\n}\n\nexport function formatMinuteWheelLabel(minute: number, locale: BsLocale): string {\n return formatTimeDigit(minute, locale)\n}\n\nexport function formatPeriodLabel(period: 'am' | 'pm', locale: BsLocale): string {\n if (locale === 'ne') {\n return period === 'am' ? 'एम' : 'पिम'\n }\n return period.toUpperCase()\n}\n",
"type": "registry:lib",
"target": "lib/bs-time-picker/time/formatters.ts"
},
{
"path": "registry/native/files/lib/bs-time-picker/time/datetime.ts",
"content": "import { getDayAdDate } from '../../bs-day-picker/calendar-grid'\nimport { getCalendarData } from '../../bs-day-picker/formatters'\nimport { getCurrentBsDate } from '../../bs-day-picker/navigation'\nimport type { BsDate } from '../../bs-day-picker/types'\nimport { clampBsTime, getDefaultBsTime } from './helpers'\nimport {\n BS_DATETIME_DISPLAY_PATTERN,\n formatBsDateTimePattern,\n} from './pattern'\nimport type { BsDateTime, BsLocale, BsTime } from './types'\n\nexport function clampBsDateTime(value: BsDateTime): BsDateTime {\n return {\n ...value,\n ...clampBsTime(value),\n }\n}\n\nexport function getDefaultBsDateTime(): BsDateTime {\n const date = getCurrentBsDate()\n const time = getDefaultBsTime()\n return { ...date, ...time }\n}\n\nexport function mergeBsDateTime(date: BsDate, time: BsTime): BsDateTime {\n return clampBsDateTime({ ...date, ...time })\n}\n\nexport function splitBsDateTime(value: BsDateTime): { date: BsDate; time: BsTime } {\n return {\n date: { year: value.year, month: value.month, day: value.day },\n time: { hour: value.hour, minute: value.minute },\n }\n}\n\nexport function formatBsDateTime(\n value: BsDateTime,\n locale: BsLocale = 'en',\n is24Hour = false,\n): string {\n return formatBsDateTimePattern(\n value,\n BS_DATETIME_DISPLAY_PATTERN,\n locale,\n is24Hour,\n )\n}\n\nfunction formatAdDateKey(date: Date): string {\n const y = date.getFullYear()\n const m = String(date.getMonth() + 1).padStart(2, '0')\n const d = String(date.getDate()).padStart(2, '0')\n return `${y}-${m}-${d}`\n}\n\n/** Map AD `Date` to BS datetime using calendar data. */\nexport function fromAdDate(date: Date): BsDateTime | null {\n const adStr = formatAdDateKey(date)\n const data = getCalendarData()\n\n for (const yearKey of Object.keys(data)) {\n const yearData = data[yearKey]\n if (!yearData) continue\n for (const monthKey of Object.keys(yearData)) {\n const monthData = yearData[monthKey]\n if (!monthData) continue\n for (const day of monthData.days) {\n if (day.adDate === adStr) {\n return {\n year: day.bsYear,\n month: day.bsMonth,\n day: day.bsDay,\n hour: date.getHours(),\n minute: date.getMinutes(),\n }\n }\n }\n }\n }\n\n return null\n}\n\n/** Map BS datetime to AD `Date` using calendar data (Asia/Kathmandu local fields). */\nexport function toAdDate(value: BsDateTime): Date | null {\n const adDate = getDayAdDate(value)\n if (!adDate) return null\n\n const [year, month, day] = adDate.split('-').map(Number)\n if (year == null || month == null || day == null) return null\n\n const date = new Date(year, month - 1, day, value.hour, value.minute, 0, 0)\n return date\n}\n",
"type": "registry:lib",
"target": "lib/bs-time-picker/time/datetime.ts"
},
{
"path": "registry/native/files/lib/bs-time-picker/time/pattern.ts",
"content": "import { pad2, toNepaliDigit } from '../../bs-day-picker/constants'\nimport { formatBsDatePattern } from '../../bs-day-picker/pattern'\nimport type { BsDate, BsLocale } from '../../bs-day-picker/types'\nimport { to12Hour } from './helpers'\nimport { formatPeriodLabel } from './formatters'\nimport type { BsDateTime, BsTime } from './types'\n\n/** Default time picker trigger pattern (12-hour). */\nexport const BS_TIME_DISPLAY_PATTERN = 'h:mm a'\n\n/** Default datetime picker trigger pattern (12-hour). */\nexport const BS_DATETIME_DISPLAY_PATTERN = 'EEEE, d MMMM yyyy, h:mm a'\n\nconst TIME_PATTERN_TOKEN =\n /'([^']*)'|(HH|H|hh|h|mm|m|a)/g\n\nconst DATETIME_PATTERN_TOKEN =\n /'([^']*)'|(EEEE|EEE|MMMM|MMM|yyyy|yy|MM|M|dd|d|HH|H|hh|h|mm|m|a)/g\n\nfunction formatHour24(hour: number, token: string, locale: BsLocale): string {\n if (token === 'HH') return pad2(hour, locale)\n return locale === 'ne' ? toNepaliDigit(hour) : String(hour)\n}\n\nfunction formatHour12(hour12: number, token: string, locale: BsLocale): string {\n if (token === 'hh') return pad2(hour12, locale)\n return locale === 'ne' ? toNepaliDigit(hour12) : String(hour12)\n}\n\nfunction formatMinute(minute: number, token: string, locale: BsLocale): string {\n if (token === 'mm') return pad2(minute, locale)\n return locale === 'ne' ? toNepaliDigit(minute) : String(minute)\n}\n\nfunction resolveTimeToken(\n time: BsTime,\n token: string,\n locale: BsLocale,\n is24Hour: boolean,\n): string {\n const { hour: hour12, period } = to12Hour(time.hour)\n\n switch (token) {\n case 'HH':\n case 'H':\n return formatHour24(time.hour, token, locale)\n case 'hh':\n case 'h':\n if (is24Hour) {\n return formatHour24(time.hour, token === 'hh' ? 'HH' : 'H', locale)\n }\n return formatHour12(hour12, token, locale)\n case 'mm':\n case 'm':\n return formatMinute(time.minute, token, locale)\n case 'a':\n if (is24Hour) return ''\n return formatPeriodLabel(period, locale)\n default:\n return token\n }\n}\n\n/** Format BS time with date-fns-style tokens. */\nexport function formatBsTimePattern(\n time: BsTime,\n pattern: string,\n locale: BsLocale = 'en',\n is24Hour = false,\n): string {\n const formatted = pattern.replace(TIME_PATTERN_TOKEN, (match, literal, token) => {\n if (literal != null) return literal\n return resolveTimeToken(time, token, locale, is24Hour)\n })\n return formatted.replace(/\\s+/g, ' ').trim()\n}\n\n/** Format BS date + time with date-fns-style tokens. */\nexport function formatBsDateTimePattern(\n value: BsDateTime,\n pattern: string,\n locale: BsLocale = 'en',\n is24Hour = false,\n): string {\n const date: BsDate = {\n year: value.year,\n month: value.month,\n day: value.day,\n }\n const time: BsTime = { hour: value.hour, minute: value.minute }\n\n const formatted = pattern.replace(\n DATETIME_PATTERN_TOKEN,\n (match, literal, token) => {\n if (literal != null) return literal\n if (\n token === 'HH' ||\n token === 'H' ||\n token === 'hh' ||\n token === 'h' ||\n token === 'mm' ||\n token === 'm' ||\n token === 'a'\n ) {\n return resolveTimeToken(time, token, locale, is24Hour)\n }\n return formatBsDatePattern(date, token, locale)\n },\n )\n\n return formatted.replace(/\\s+,/g, ',').replace(/\\s+/g, ' ').trim()\n}\n",
"type": "registry:lib",
"target": "lib/bs-time-picker/time/pattern.ts"
},
{
"path": "registry/native/files/lib/bs-time-picker/time/index.ts",
"content": "export * from './types'\nexport * from './helpers'\nexport * from './formatters'\nexport * from './datetime'\nexport * from './pattern'\n",
"type": "registry:lib",
"target": "lib/bs-time-picker/time/index.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-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\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-time-picker-wheels.ios.tsx",
"content": "import {\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 { BsLocale, BsPeriod, BsTime } from '@/lib/bs-time-picker/time/types'\nimport {\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 BsTimePickerWheelsProps = {\n value: BsTime\n locale: BsLocale\n is24Hour?: boolean\n onChange: (value: BsTime) => void\n}\n\nexport function BsTimePickerWheelColumns({\n value,\n locale,\n is24Hour = false,\n onChange,\n}: BsTimePickerWheelsProps) {\n const clamped = clampBsTime(value)\n const hourOptions = getHourOptions(is24Hour)\n const minuteOptions = getMinuteOptions()\n const periodOptions = getPeriodOptions()\n\n const displayHour = resolveDisplayHour(clamped.hour, is24Hour)\n const displayPeriod = resolveDisplayPeriod(clamped.hour, is24Hour)\n\n const updateTime = (\n nextHour: number,\n nextMinute: number,\n nextPeriod: BsPeriod,\n ) => {\n onChange(\n clampBsTime({\n hour: resolveWheelHour(nextHour, nextPeriod, is24Hour),\n minute: nextMinute,\n }),\n )\n }\n\n return (\n <>\n updateTime(hour, clamped.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, clamped.minute, period)\n }\n formatLabel={(period) => formatPeriodOption(period, locale)}\n />\n ) : null}\n >\n )\n}\n\nexport function BsTimePickerWheels({\n ...props\n}: BsTimePickerWheelsProps) {\n return (\n \n \n \n )\n}\n",
"type": "registry:component",
"target": "components/ui/bs-time-picker-wheels.ios.tsx"
},
{
"path": "registry/native/files/components/ui/bs-time-picker.ios.tsx",
"content": "import { getDefaultBsTime } from '../../lib/bs-time-picker'\nimport {\n BS_TIME_DISPLAY_PATTERN,\n formatBsTimePattern,\n} from '@/lib/bs-time-picker/time/pattern'\nimport type { BsLocale, BsTime } from '@/lib/bs-time-picker/time/types'\nimport { cn } from '@/lib/utils'\nimport { Clock } 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 { BsTimePickerWheels } from './bs-time-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 BsTimePickerProps = {\n value?: BsTime\n onValueChange?: (value: BsTime | undefined) => void\n locale?: BsLocale\n is24Hour?: boolean\n placeholder?: string\n cancelLabel?: string\n confirmLabel?: string\n formatPattern?: string\n formatValue?: (value: BsTime, locale: BsLocale, is24Hour: boolean) => string\n className?: string\n disabled?: boolean\n}\n\nfunction resolveTimeDisplayLabel(\n value: BsTime,\n locale: BsLocale,\n is24Hour: boolean,\n formatValue?: BsTimePickerProps['formatValue'],\n formatPattern?: string,\n): string {\n if (formatValue) return formatValue(value, locale, is24Hour)\n return formatBsTimePattern(\n value,\n formatPattern ?? BS_TIME_DISPLAY_PATTERN,\n locale,\n is24Hour,\n )\n}\n\nexport function BsTimePicker({\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}: BsTimePickerProps) {\n const insets = useSafeAreaInsets()\n const resolvedPlaceholder =\n placeholder ?? (locale === 'ne' ? 'समय छान्नुहोस्' : 'Select time')\n const resolvedCancel = cancelLabel ?? 'Cancel'\n const resolvedConfirm = confirmLabel ?? 'Confirm'\n const [showPicker, setShowPicker] = React.useState(false)\n const [tempTime, setTempTime] = React.useState(\n value ?? getDefaultBsTime(),\n )\n\n const handleOpen = () => {\n if (disabled) return\n setTempTime(value ?? getDefaultBsTime())\n setShowPicker(true)\n }\n\n const handleCancel = () => {\n setShowPicker(false)\n setTempTime(value ?? getDefaultBsTime())\n }\n\n const handleConfirm = () => {\n setShowPicker(false)\n onValueChange?.(tempTime)\n }\n\n const displayLabel = value\n ? resolveTimeDisplayLabel(\n value,\n locale,\n is24Hour,\n formatValue,\n formatPattern,\n )\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-time-picker.ios.tsx"
},
{
"path": "registry/native/files/components/ui/bs-time-picker.tsx",
"content": "export { BsTimePicker, type BsTimePickerProps } from './bs-time-picker.ios'\n",
"type": "registry:component",
"target": "components/ui/bs-time-picker.tsx"
},
{
"path": "registry/native/files/components/ui/bs-time-picker.android.tsx",
"content": "import { getDefaultBsTime } from '../../lib/bs-time-picker'\nimport {\n BS_TIME_DISPLAY_PATTERN,\n formatBsTimePattern,\n} from '@/lib/bs-time-picker/time/pattern'\nimport type { BsLocale, BsTime } from '@/lib/bs-time-picker/time/types'\nimport { cn } from '@/lib/utils'\nimport { Clock } from 'lucide-react-native'\nimport * as React from 'react'\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 BsTimePickerProps = {\n value?: BsTime\n onValueChange?: (value: BsTime | 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?: (value: BsTime, locale: BsLocale, is24Hour: boolean) => string\n className?: string\n disabled?: boolean\n}\n\nfunction resolveTimeDisplayLabel(\n value: BsTime,\n locale: BsLocale,\n is24Hour: boolean,\n formatValue?: BsTimePickerProps['formatValue'],\n formatPattern?: string,\n): string {\n if (formatValue) return formatValue(value, locale, is24Hour)\n return formatBsTimePattern(\n value,\n formatPattern ?? BS_TIME_DISPLAY_PATTERN,\n locale,\n is24Hour,\n )\n}\n\nexport function BsTimePicker({\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}: BsTimePickerProps) {\n const resolvedPlaceholder =\n placeholder ?? (locale === 'ne' ? 'समय छान्नुहोस्' : 'Select time')\n const resolvedTitle = title ?? resolvedPlaceholder\n const resolvedCancel = cancelLabel ?? 'Cancel'\n const resolvedConfirm = confirmLabel ?? 'OK'\n const [showPicker, setShowPicker] = React.useState(false)\n const [tempTime, setTempTime] = React.useState(\n value ?? getDefaultBsTime(),\n )\n\n const handleOpen = () => {\n if (disabled) return\n setTempTime(value ?? getDefaultBsTime())\n setShowPicker(true)\n }\n\n const handleCancel = () => {\n setShowPicker(false)\n setTempTime(value ?? getDefaultBsTime())\n }\n\n const handleConfirm = () => {\n setShowPicker(false)\n onValueChange?.(tempTime)\n }\n\n const displayLabel = value\n ? resolveTimeDisplayLabel(\n value,\n locale,\n is24Hour,\n formatValue,\n formatPattern,\n )\n : resolvedPlaceholder\n\n return (\n <>\n \n\n \n >\n )\n}\n",
"type": "registry:component",
"target": "components/ui/bs-time-picker.android.tsx"
}
],
"docs": "Installs RNR `button` (and `text`) + `icon` when missing. Install bs-calendar and bs-date-picker first. Android uses Material-style `BsTimePickerDialog` (no native time picker).",
"type": "registry:component"
}