# React Hooks

A collection of useful React custom hooks for common functionalities.

## Installation

```bash
npm install codedsaif-react-hooks
```

## Available Hooks

### useClickOutside

A lightweight React hook to detect clicks outside a referenced component. Perfect for dropdowns, modals, and popovers.

```jsx
/**
 * Custom hook that triggers a callback when a user clicks outside the referenced element.
 * @param {Function} callback - The function to call when clicking outside the element.
 * @returns {React.RefObject} - A ref that should be attached to the element you want to monitor.
 * @example
 * function Dropdown() {
 *   const [isOpen, setIsOpen] = useState(false);
 *   const dropdownRef = useClickOutside(() => setIsOpen(false));
 *   return (
 *     <div>
 *       <button onClick={() => setIsOpen(!isOpen)}>Toggle Menu</button>
 *       {isOpen && (
 *         <ul ref={dropdownRef}>
 *           <li>Option 1</li>
 *           <li>Option 2</li>
 *           <li>Option 3</li>
 *       </ul>
 *     )}
 *   </div>
 * );
 */
```

### useDebounce

Debounces a value with a specified delay.

```jsx
/**
 * Custom hook to debounce a value after a specified delay.
 * @param {*} value - The value to debounce.
 * @param {number} [delay=500] - Delay in milliseconds before updating the debounced value.
 * @returns {*} - The debounced value.
 * @example
 * const [searchTerm, setSearchTerm] = useState("");
 * const debouncedSearch = useDebounce(searchTerm, 300);
 * useEffect(() => {
 *   if (debouncedSearch) {
 *     fetch(`https://api.example.com/search?q=${debouncedSearch}`)
 *       .then((res) => res.json())
 *       .then((data) => console.log(data));
 *   }
 * }, [debouncedSearch]);
 * return (
 *   <input
 *     type="text"
 *     placeholder="Search..."
 *     onChange={(e) => setSearchTerm(e.target.value)}
 *   />
 * );
 */
```

### useFetch

Simplifies data fetching with loading and error states.

```jsx
/**
 * Custom hook to fetch data from an API.
 * @param {string} url - The URL to fetch data from.
 * @param {object} options - Optional fetch options (headers, method, etc.).
 * @returns {{ data: any, loading: boolean, error: string|null }} - The fetched data, loading state, and error message.
 * @example
 * const { data, loading, error } = useFetch("https://api.example.com/users");
 * if (loading) return <p>Loading...</p>;
 * if (error) return <p>Error: {error}</p>;
 * return (
 *   <ul>
 *     {data.map(user => (
 *       <li key={user.id}>{user.name}</li>
 *     ))}
 *   </ul>
 * );
 */
```

### useLocalStorage

Persists state in localStorage with automatic JSON parsing/stringifying.

```jsx
/**
 * Custom hook to persist state in localStorage.
 * @param {string} key - The localStorage key to store the value under.
 * @param {*} initialValue - The initial value to use if none is found in localStorage.
 * @returns {[any, function]} - Returns the stored value and a setter function.
 * @example
 * function Dropdown() {
 *   const [theme, setTheme] = useLocalStorage("theme", "light");
 *   useEffect(() => {
 *     document.body.className = theme;
 *   }, [theme]);
 *   return (
 *     <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
 *       Toggle Theme
 *     </button>
 *   );
 * }
 */
```

### usePrevious

Tracks the previous value of a state or prop.

```jsx
/**
 * Custom hook to get the previous value of a prop or state.
 * @param {*} value - The current value to track.
 * @returns {*} - The previous value before the current render.
 * @example
 * const [count, setCount] = useState(0);
 * const prevCount = usePrevious(count);
 * return (
 *   <>
 *     <p>Current: {count}, Previous: {prevCount}</p>
 *     <button onClick={() => setCount(count + 1)}>Increase</button>
 *   </>
 * );
 */
```

### useTheme

Manages theme switching with localStorage persistence.

```jsx
/**
 * Custom hook to manage dark and light theme modes,
 * persisting the theme preference in localStorage.
 * @returns {[string, function]} - Current theme and a toggle function.
 * @example
 * const [theme, toggleTheme] = useDarkMode();
 * return (
 *   <button onClick={toggleTheme}>
 *     Switch to {theme === "light" ? "Dark" : "Light"} Mode
 *   </button>
 * );
 */
```

### useWindowSize

Tracks window dimensions with responsive updates.

```jsx
/**
 * Custom hook to get the current window dimensions.
 * @returns {{ width: number, height: number }} - The current width and height of the window.
 * @example
 * function ResponsiveComponent() {
 *   const { width } = useWindowSize();
 *   return (
 *     <div>
 *       {width > 1024 ? (
 *         <h1>Desktop View 🖥️</h1>
 *       ) : width > 768 ? (
 *         <h1>Tablet View 📱</h1>
 *       ) : (
 *         <h1>Mobile View 📲</h1>
 *       )}
 *   </div>
 * );
 */
```

### useInfiniteScroll
Supports both automatic callback fetching and manual slicing with scroll.

```jsx
/**
 * @param {Function|null} callback - Called when scroll reaches bottom (set null for manual mode)
 * @param {Object} options - Configuration options
 * @param {React.RefObject} [options.containerRef=null] - Scrollable container ref
 * @param {boolean} [options.manual=false] - If true, enables manual mode and returns 'limit'
 * @param {number} [options.start=10] - Initial limit in manual mode
 * @param {number} [options.pace=10] - Increment step for limit
 * @param {number} [options.offset=100] - Distance from bottom to trigger scroll (in px)
 * @param {boolean} [options.hasMore=true] - Set false to stop infinite loading when no more data
 * @param {boolean} [options.debug=false] - Enable funny console logs
 * 
 * @returns {[boolean, Function]|number}
 * - If manual=false: returns [isFetching, setIsFetching]
 * - If manual=true: returns limit (number)
 *
 * Example: Auto-fetch Mode (Default)
 * -------------------------------------
 * const containerRef = useRef(null);
 * const [data, setData] = useState([]);
 * const [page, setPage] = useState(1);
 * const [hasMore, setHasMore] = useState(true);
 * const fetchMore = async () => {
 *   const res = await fetch(`https://medrum.herokuapp.com/feeds/?source=5718e53e7a84fb1901e05971&page=${page}&sort=latest`);
 *   const json = await res.json();
 *   if (json.length === 0) setHasMore(false);
 *   // OR if(json.count === data.length) setHasMore(false);
 *   else {
 *     setData(prev => [...prev, ...json]);
 *     setPage(prev => prev + 1);
 *   }
 * };
 * const [isFetching, setIsFetching] = useInfiniteScroll(fetchMore, { containerRef, hasMore });
 *
 * Example: Manual Slice Mode
 * ------------------------------
 * const containerRef = useRef(null);
 * const [data, setData] = useState([]);
 * const [page, setPage] = useState(1);
 * const [hasMore, setHasMore] = useState(true);
 * const limit = useInfiniteScroll(null, { containerRef, manual: true, start: 10, pace: 10, hasMore });
 * const fetchMore = async () => {
 *   const res = await fetch(`https://medrum.herokuapp.com/feeds/?source=5718e53e7a84fb1901e05971&page=${page}&sort=latest`);
 *   const json = await res.json();
 *   if (json.length === 0) setHasMore(false);
 *   else {
 *     setData(prev => [...prev, ...json]);
 *     setPage(prev => prev + 1);
 *   }
 * };
 * useEffect(() => {
 *   fetchMore();
 * }, [limit]);
 * return (
 *   <ul ref={containerRef} style={{ height: "400px", overflowY: "scroll" }}>
 *     {data.slice(0, limit).map(item => (
 *       <li key={item.id}>{item.title}</li>
 *     ))}
 *   </ul>
 * );
 */
```

### useInterval
Runs a callback at a specified interval with support for pausing, debugging, initial delay, and external control.

```jsx
/**
 * @param {Function} callback - Function to run at each interval tick.
 * @param {number|null} delay - Interval duration in ms. Use `null` to pause.
 * @param {Object} [options={}] - Additional options.
 * @param {boolean} [options.debug=false] - Log lifecycle events to the console.
 * @param {boolean} [options.stop=false] - Stops interval immediately when set to true.
 * @param {boolean} [options.immediate=false] - Fires callback immediately on mount.
 * @param {number} [options.initialDelay] - Wait this long before starting interval loop.
 * @param {number} [options.maxTicks] - Stop after this many ticks.
 *
 * @returns {
 *   count: number,
 *   reset: () => void,
 *   forceTick: () => void,
 *   pause: () => void,
 *   resume: () => void,
 * }
 * 
 * @example
 * // Logs "Tick" every second
 * useInterval(() => console.log('Tick'), 1000);
 *
 * @example
 * const { count, reset } = useInterval(() => {
 *   console.log('Tick!');
 * }, 1000);
 * 
 * @example
 * // With pause/resume capability
 * const [delay, setDelay] = useState(1000);
 * <button onClick={() => setDelay(prev => prev ? null : 1000)}>Toggle</button>
 * useInterval(() => console.log('Toggling...'), delay);
 * 
 * @example
 * function CountDown({ initialCount, downLimit, initialDelay = 1000, bgColor = 'transparent' }) {
 *     const [count, setCount] = useState(initialCount);
 *     const [delay, setDelay] = useState(null);
 *     useInterval(() => {
 *         const newValue = count - 1;
 *         setCount(newValue)
 *         if (newValue <= downLimit) setDelay(null);
 *     }, delay);
 *     return (
 *         <div style={{ display: 'flex', backgroundColor: bgColor }}>{count}
 *             <button onClick={() => setDelay(initialDelay)}>Start</button>
 *             <button onClick={() => setDelay(null)}>Stop</button>
 *         </div>
 *     )
 * }
 * 
 * @example
 * import React, { useState, useCallback } from 'react';
 * import useInterval from './useInterval';
 * const ShoppingList = () => {
 *     // Wait 5 seconds before fetching new data
 *     const POLL_DELAY = 5000;
 *     const [items, setItems] = useState([]);
 *     const fetchItems = useCallback(async () => {
 *         const response = await fetch('/shopping-list/items');
 *         const json = await response.json();
 *         setItems(json);
 *     }, []);
 *     useEffect(() => {
 *         // Fetch items from API on mount
 *         fetchItems();
 *     }, []);
 *     useInterval(() => {
 *         fetchItems();
 *     }, POLL_DELAY);
 *     return (
 *         <ul>
 *             {items.map((item) => <li>{item.title}</li>)}
 *         </ul>
 *     )
 * };
 */
```

### useKeyPress
A powerful custom React hook for handling keyboard interactions.

```jsx
/**
 * Features:
 * - Detect and respond to individual key presses
 * - Match specific target key (e.g. 'a')
 * - Maintain history of pressed keys
 * - Detect modifier key states: Shift, Alt, Ctrl, Meta
 * - Detect multi-key combos like 'ctrl+s', 'cmd+z'
 * - Pause and resume key tracking
 * - Optionally ignore key presses in input, textarea, or contentEditable fields
 *
 * @param {Object} options - Configuration options
 * @param {Function} [options.onKeyPress] - Function to run when a valid key is pressed
 * @param {string} [options.targetKey] - Key to match for triggering `keyMatched` (case-insensitive)
 * @param {boolean} [options.enableHistory=true] - Whether to track history of pressed keys
 * @param {number} [options.maxHistory=10] - Max number of keys to store in history
 * @param {boolean} [options.ignoreInput=false] - If true, ignores key presses from editable elements (input, textarea, contentEditable)
 * @param {string[]} [options.combos=[]] - List of keyboard combos to detect (e.g. ['ctrl+s', 'cmd+z'])
 *
 * @returns {Object} Hook output
 * @returns {string|null} keyPressed - The current key being pressed (null if none)
 * @returns {boolean} keyMatched - True if the most recent key matched `targetKey`
 * @returns {string[]} keyHistory - Array of previously pressed keys (up to `maxHistory`)
 * @returns {string|null} comboMatched - The matched combo string, if any (e.g. 'ctrl+s')
 * @returns {Object} modifiers - Current modifier key states { shift, alt, ctrl, meta }
 * @returns {Function} reset - Clears the keyMatched, keyHistory, and comboMatched states
 * @returns {Function} setPaused - Function to pause/resume the hook (use `setPaused(true)` to pause)
 *
 * @example
 * // Basic key detection and match tracking
 * const { keyPressed, keyMatched, keyHistory } = useKeyPress({
 *   onKeyPress: (key) => console.log('You pressed:', key),
 *   targetKey: 'a',
 *   enableHistory: true,
 *   maxHistory: 5,
 * });
 *
 * @example
 * // Full usage with modifiers, combo detection, pause and reset
 * const {
 *   keyPressed,
 *   keyMatched,
 *   keyHistory,
 *   comboMatched,
 *   modifiers,
 *   reset,
 *   setPaused,
 * } = useKeyPress({
 *   onKeyPress: (key) => console.log('Key:', key),
 *   targetKey: 's',
 *   combos: ['ctrl+s', 'cmd+z'],
 *   ignoreInput: true,
 * });
 *
 * useEffect(() => {
 *   if (comboMatched === 'ctrl+s') {
 *     console.log('Save triggered!');
 *   }
 * }, [comboMatched]);
 */
```

## License

This package is licensed under the [MIT License](https://opensource.org/licenses/MIT).