"use client"; import * as React from "react"; import { cn } from "@/lib/utils"; // --------------------------------------------------------------------------------------------------------------------- // Context ------------------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- interface WheelPickerContextValue { items: string[]; currentIndex: number; onIndexChange: (index: number) => void; theta: number; // angle between two items radius: number; // translateZ distance count: number; // items including placeholders } const WheelPickerContext = React.createContext( null, ); const useWheelPickerContext = () => { const ctx = React.useContext(WheelPickerContext); if (!ctx) { throw new Error( "[WheelPicker] sub component must be rendered within root", ); } return ctx; }; // --------------------------------------------------------------------------------------------------------------------- // WheelPicker (Root / Provider) --------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- export interface WheelPickerProps extends React.HTMLAttributes { items: string[]; /** 0-based index of the currently selected item. Defaults to 0. */ currentIndex: number; /** Callback that is invoked with the currently selected item (string) whenever the selection changes */ onIndexChange: (index: number) => void; /** Radius (in `px`) of the carousel – tweak to fit line height of text (Default: 28) */ radius?: number; } const WheelPicker = React.forwardRef( ( { items, currentIndex, onIndexChange, radius: radiusProp = 28, className, children, ...props }, ref, ) => { // internal render count includes two placeholders at start & end const count = items.length + 2; const theta = (2 * Math.PI) / count; const contextValue = React.useMemo( () => ({ items, currentIndex, onIndexChange, theta, radius: radiusProp, count, }), [items, currentIndex, onIndexChange, theta, radiusProp, count], ); return (
{children}
); }, ); WheelPicker.displayName = "WheelPicker"; // --------------------------------------------------------------------------------------------------------------------- // WheelPickerSelect --------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- export type WheelPickerSelectProps = React.HTMLAttributes; const WheelPickerSelect = React.forwardRef< HTMLDivElement, WheelPickerSelectProps >(({ className, children, ...props }, ref) => { const { items, currentIndex, onIndexChange } = useWheelPickerContext(); const moveBy = React.useCallback( (delta: number) => { const itemsLength = items.length; let newIndex = currentIndex + delta; // Handle wrapping if (newIndex < 0) { newIndex = itemsLength - 1; } else if (newIndex >= itemsLength) { newIndex = 0; } onIndexChange(newIndex); }, [currentIndex, items.length, onIndexChange], ); const handleKeyDown = React.useCallback( (e: React.KeyboardEvent) => { switch (e.key) { case "ArrowUp": case "ArrowLeft": e.preventDefault(); moveBy(1); break; case "ArrowDown": case "ArrowRight": e.preventDefault(); moveBy(-1); break; case "Home": e.preventDefault(); onIndexChange(0); break; case "End": e.preventDefault(); onIndexChange(items.length - 1); break; } }, [moveBy, onIndexChange, items.length], ); return (
{children}
); }); WheelPickerSelect.displayName = "WheelPickerSelect"; // --------------------------------------------------------------------------------------------------------------------- // WheelPickerOptions -------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- export type WheelPickerOptionsProps = React.HTMLAttributes; const WheelPickerOptions = React.forwardRef< HTMLDivElement, WheelPickerOptionsProps >(({ className, ...props }, ref) => { const { items, currentIndex, onIndexChange, theta, radius } = useWheelPickerContext(); return (
{/* First placeholder */} {/* Real items */} {items.map((item, idx) => { // Render index = item index + 1 (accounting for first placeholder) const renderIdx = idx + 1; const angle = theta * renderIdx; const isSelected = idx === currentIndex; return (
{ if (!isSelected) { e.preventDefault(); e.stopPropagation(); onIndexChange(idx); } }} > {item}
); })} {/* Last placeholder */}
); }); WheelPickerOptions.displayName = "WheelPickerOptions"; // --------------------------------------------------------------------------------------------------------------------- // WheelPickerEmpty (placeholders) ------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- type PlaceholderPosition = "first" | "last"; export interface WheelPickerEmptyProps extends React.HTMLAttributes { position: PlaceholderPosition; } const WheelPickerEmpty = React.forwardRef< HTMLDivElement, WheelPickerEmptyProps >(({ position, className, ...props }, ref) => { const { theta, radius, count, currentIndex } = useWheelPickerContext(); const renderIdx = position === "first" ? 0 : count - 1; const angle = theta * renderIdx; return (
{ e.preventDefault(); e.stopPropagation(); }} {...props} /> ); }); WheelPickerEmpty.displayName = "WheelPickerEmpty"; // --------------------------------------------------------------------------------------------------------------------- // Exports ------------------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- export { WheelPicker, WheelPickerSelect, WheelPickerOptions, WheelPickerEmpty };