Dropdown

Select an option
  <Dropdown
      className={'" w-52"'}
      placeholder={'"Select an option"'}
      multiple={false}
      value={""}
      onChange={handleDropdownChange}
  >
      <DropdownItem>Option 1</DropdownItem>
      <DropdownItem>Choice 2</DropdownItem>
      <DropdownItem>Selection 3</DropdownItem>
      <DropdownItem>Selection 4</DropdownItem>
  </Dropdown>

"use client";

import React, { useState, useRef, useEffect, createContext, useContext } from 'react';

const DropdownContext = createContext();

/**
 * @typedef {Object} DropdownProps
 * @property {string} [className] - Additional CSS class names to apply to the dropdown container.
 * @property {React.ReactNode} children - The dropdown items to be displayed.
 * @property {string|number} [value] - The currently selected value (for single selection).
 * @property {function} [onChange] - A callback function that is triggered when the selected value changes.
 * @property {string} [placeholder] - The placeholder text to display when no value is selected.
 * @property {boolean} [multiple] - Whether to allow multiple selections. Defaults to false.
 * 
 * A customizable dropdown component for selecting single or multiple values.
 * 
 * @param {DropdownProps} props - The properties for the dropdown component.
 * @returns {JSX.Element} The rendered dropdown component.
 */
export default function Dropdown({ className, children, value, onChange, placeholder = "Select an option", multiple = false }) {
    const [isOpen, setIsOpen] = useState(false);
    const [search, setSearch] = useState("");
    const [multipleSelection, setMultipleSelection] = useState([]);
    const [positionUpwards, setPositionUpwards] = useState(false);
    const dropdownRef = useRef(null);

    // Context value to share state with DropdownItem
    const contextValue = { handleChange, multipleSelection, multiple };

    // Determine dropdown positioning (upward or downward)
    useEffect(() => {
        if (isOpen && dropdownRef.current) {
            const rect = dropdownRef.current.getBoundingClientRect();
            setPositionUpwards(window.innerHeight - rect.bottom < 200); // Open upwards if not enough space below
        }
    }, [isOpen]);

    /**
     * Handles changes in the selected value(s).
     * 
     * @param {string|number} newValue - The new selected value.
     */
    function handleChange(newValue) {
        if (onChange && multiple) {
            if (!multipleSelection.includes(newValue)) {
                const newSelection = [...multipleSelection, newValue];
                setMultipleSelection(newSelection);
                onChange(newSelection);
            } else {
                const filteredSelection = multipleSelection.filter(item => item !== newValue);
                setMultipleSelection(filteredSelection);
                onChange(filteredSelection);
            }
            setSearch("");
        } else if (onChange) {
            onChange(newValue);
            setIsOpen(false);
            setSearch("");
        }
    }

    // Filter children for search if there are more than 5 items
    const filteredChildren = React.Children.count(children) > 5
        ? React.Children.toArray(children).filter(child => {
            const content = child.props.value || child.props.children;
            return content.toLowerCase().includes(search.toLowerCase());
        })
        : children;

    return (
        <DropdownContext.Provider value={contextValue}>

            <style>
                {`
                /* Custom Scrollbar Styles */
                .custom-scrollbar::-webkit-scrollbar {
                    width: 10px;
                    height: 10px;
                }

                .custom-scrollbar::-webkit-scrollbar-track {
                    background-color: #333;
                    border-radius: 8px;
                }

                .custom-scrollbar::-webkit-scrollbar-thumb {
                    background-color: #555;
                    border-radius: 8px;
                    border: 2px solid transparent;
                    background-clip: content-box;
                }

                .custom-scrollbar::-webkit-scrollbar-thumb:hover {
                    background-color: #444;
                }

                .custom-scrollbar {
                    scrollbar-width: thin;
                    scrollbar-color: #555 #333;
                    scrollbar-gutter: stable;
                }
                `}
            </style>
            <div className={`relative mt-1 ${className}`} ref={dropdownRef}>
                <div onClick={() => setIsOpen(!isOpen)}
                    className="placeholder:text-gray-400 hover:bg-opacity-70 bg-gray-900 cursor-pointer p-2 border rounded-md relative w-full flex items-center justify-between border-gray-500 shadow-sm sm:text-sm focus:border disabled:bg-gray-900/90 disabled:cursor-not-allowed"
                >
                    <div className='flex items-center gap-1 flex-wrap'>
                        {multiple && multipleSelection.length > 0 ? (
                            multipleSelection.map((item, index) => (
                                <span key={index} className='px-2 py-1 bg-slate-500/90 text-sm rounded-md'>{item}</span>
                            ))
                        ) : value ? (
                            <span>{value}</span>
                        ) : (
                            <span className="text-gray-500">{placeholder}</span>
                        )}
                    </div>
                    <svg
                        className={`w-5 h-5 transition-transform transform ${isOpen ? 'rotate-180' : ''}`}
                        viewBox="0 0 20 20"
                        fill="currentColor"
                    >
                        <path
                            fillRule="evenodd"
                            d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
                            clipRule="evenodd"
                        />
                    </svg>
                </div>
                {isOpen && (
                    <div className={`absolute z-10 border rounded-md overflow-hidden  shadow-md mt-1 w-full ${positionUpwards ? 'bottom-full mb-1' : 'top-full mt-1'}`}>
                        <ul className="max-h-80 overflow-auto custom-scrollbar py-1 flex flex-col gap-1 bg-gray-900">
                            {(React.Children.count(children) > 5) && (
                                <div className="px-2 py-1">
                                    <input
                                        type="text"
                                        className="border bg-black rounded-md px-1 py-1 w-full"
                                        value={search}
                                        onChange={(e) => setSearch(e.target.value)}
                                        placeholder="Search..."
                                    />
                                </div>
                            )}
                            {filteredChildren.map((child, index) => (
                                <li key={index}>{child}</li>
                            ))}
                        </ul>
                    </div>
                )}
            </div>
        </DropdownContext.Provider>
    );
}

/**
 * @typedef {Object} DropdownItemProps
 * @property {React.ReactNode} children - The content of the dropdown item.
 * @property {string|number} [value] - The value associated with the dropdown item.
 * 
 * @param {DropdownItemProps} props - The properties for the dropdown item.
 * @returns {JSX.Element} The rendered dropdown item.
 */
export const DropdownItem = ({ children, value }) => {
    const { handleChange, multipleSelection, multiple } = useContext(DropdownContext);

    return (
        <div
            onClick={() => handleChange(value || children)}
            className={`px-2 py-2 cursor-pointer hover:bg-gray-800/90 ${multiple && multipleSelection.includes(value || children) ? "bg-gray-800/90" : ""}`}
        >
            {children}
        </div>
    );
};