Skip to content

Commit 8afe3d1

Browse files
committed
feat: create registry
1 parent fe42115 commit 8afe3d1

File tree

11 files changed

+227
-0
lines changed

11 files changed

+227
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "use-boolean",
4+
"type": "registry:hook",
5+
"files": [
6+
{
7+
"path": "registry/hooks/use-boolean/index.ts",
8+
"content": "import { useCallback, useMemo, useState } from \"react\"\n\ntype UseBooleanReturn = {\n value: boolean\n setTrue: () => void\n setFalse: () => void\n}\n\n/**\n * Manage a boolean state, with additional convenience utility methods.\n *\n * @param `initialValue` - Initial value of the boolean state. If not provided, it defaults to `false`.\n * @returns An object containing:\n * - `value`: The current boolean state\n * - `setFalse`: A function to set the boolean state to `false`\n * - `setTrue`: A function to set the boolean state to `true`\n *\n * @example\n * ```tsx\n * import { useBoolean } from \"@yeskunall/react-hooks\";\n *\n * function ModalDemo() {\n * const { value: isOpen, setFalse: close, setTrue: open } = useBoolean(false);\n *\n * return (\n * <div>\n * <button onClick={open}>Open Modal</button>\n *\n * {isOpen && (\n * <div className=\"modal\">\n * <p>This is a modal!</p>\n * <button onClick={close}>Close</button>\n * </div>\n * )}\n * </div>\n * );\n * }\n * ```\n */\nexport function useBoolean(initialValue: boolean = false): UseBooleanReturn {\n const [value, setValue] = useState<boolean>(initialValue)\n\n const setFalse = useCallback(() => setValue(false), [])\n const setTrue = useCallback(() => setValue(true), [])\n\n return useMemo<UseBooleanReturn>(\n () => ({\n setFalse,\n setTrue,\n value,\n }),\n [setFalse, setTrue, value],\n )\n}\n",
9+
"type": "registry:hook"
10+
}
11+
]
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "use-click-anywhere",
4+
"type": "registry:hook",
5+
"files": [
6+
{
7+
"path": "registry/hooks/use-click-anywhere/index.ts",
8+
"content": "import { useEffect } from \"react\"\n\n/**\n * A React hook that handles click events anywhere on the window.\n *\n * @param handler - The function to be called when a click event is detected anywhere on the window.\n *\n * @example\n * ```tsx\n * import { useClickAnywhere } from \"@yeskunall/react-hooks\"\n *\n * function Component() {\n * useClickAnywhere((event) => {\n * console.log(\"User interaction:\", event.type, event.target);\n * });\n *\n * return <button>Click or tap me (or anywhere else)</button>;\n * }\n * ```\n */\nexport function useClickAnywhere(handler: (event: MouseEvent) => void) {\n useEffect(() => {\n window.addEventListener(\"click\", handler)\n\n return () => window.removeEventListener(\"click\", handler)\n }, [handler])\n}\n",
9+
"type": "registry:hook"
10+
}
11+
]
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "use-counter",
4+
"type": "registry:hook",
5+
"files": [
6+
{
7+
"path": "registry/hooks/use-counter/index.ts",
8+
"content": "import { useCallback, useMemo, useState } from \"react\"\n\nimport type { Dispatch, SetStateAction } from \"react\"\n\ninterface UseCounterReturn {\n count: number\n decrement: (delta?: number) => void\n increment: (delta?: number) => void\n reset: () => void\n set: Dispatch<SetStateAction<number>>\n}\n\n/**\n * Manage a numeric counter with convenient utility methods.\n *\n * @param `initialValue` - Initial value of the counter state. If not provided, it defaults to 0.\n * @returns An object with the current count and helper methods to update it.\n *\n * @example\n * ```tsx\n * import { useCounter } from \"@yeskunall/react-hooks\";\n *\n * function Pagination() {\n * const { count: page, decrement, increment, reset } = useCounter(1);\n *\n * return (\n * <div>\n * <button onClick={() => decrement()} disabled={page <= 1}>\n * Prev\n * </button>\n * <span>Page {page}</span>\n * <button onClick={() => increment()}>Next</button>\n * <button onClick={reset}>Reset</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useCounter(initialValue: number = 0): UseCounterReturn {\n const [count, setCount] = useState(initialValue)\n\n const decrement = useCallback((delta = 1) => setCount(previous => previous - delta), [])\n const increment = useCallback((delta = 1) => setCount(previous => previous + delta), [])\n const reset = useCallback(() => setCount(initialValue), [initialValue])\n\n return useMemo<UseCounterReturn>(\n () => ({\n count,\n decrement,\n increment,\n reset,\n set: setCount,\n }),\n [count, decrement, increment, reset],\n )\n}\n",
9+
"type": "registry:hook"
10+
}
11+
]
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "use-cycle",
4+
"type": "registry:hook",
5+
"files": [
6+
{
7+
"path": "registry/hooks/use-cycle/index.ts",
8+
"content": "import { useState } from \"react\"\n\n/**\n * Cycle through a sequence of values each time a function is called.\n *\n * @param values - A non-empty array of values to cycle through.\n * @returns A tuple containing the following:\n * - The current value\n * - A function that changes the current value to the next one in the sequence, or the first one if the current value is the last in the sequence.\n *\n * @example\n * ```tsx\n * import { useCycle } from \"@yeskunall/react-hooks\";\n *\n * function ThemeSwitcher() {\n * const { value: theme, cycle } = useCycle(\"light\", \"dark\", \"system\");\n *\n * return (\n * <div>\n * <p>Current theme: {theme}</p>\n * <button onClick={cycle}>Switch Theme</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useCycle<T>(...args: T[]) {\n const [index, setIndex] = useState(0)\n const value = args[index]\n\n const cycle = () => {\n setIndex(previous => (previous + 1) % args.length)\n }\n\n return [value, cycle] as const\n}\n",
9+
"type": "registry:hook"
10+
}
11+
]
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "use-debounce",
4+
"type": "registry:hook",
5+
"files": [
6+
{
7+
"path": "registry/hooks/use-debounce/index.ts",
8+
"content": "import { useEffect, useState } from \"react\"\n\n/**\n * Delay state updates until a specified `delay` has passed without any further changes to the provided `value`.\n *\n * @typeParam T - Type of the input value being debounced.\n *\n * @param value - Input value to debounce.\n * @param delay - Debounce delay in milliseconds.\n * @returns Debounced value.\n *\n * @example\n * ```tsx\n * import { useDebounce } from \"@yeskunall/react-hooks\";\n *\n * function SearchInput() {\n * const [query, setQuery] = useState(\"\");\n * const debouncedQuery = useDebounce(query, 500);\n *\n * useEffect(() => {\n * if (debouncedQuery) {\n * // Trigger API request with debounced query\n * fetch(`/api/search?q=${debouncedQuery}`);\n * }\n * }, [debouncedQuery]);\n *\n * return (\n * <input\n * value={query}\n * onChange={(e) => setQuery(e.target.value)}\n * placeholder=\"Type to search...\"\n * />\n * );\n * }\n * ```\n */\nexport function useDebounce<T>(value: T, delay: number) {\n const [debounced, setDebounced] = useState<T>(value)\n\n useEffect(() => {\n const id = setTimeout(() => {\n setDebounced(value)\n }, delay)\n\n return () => clearTimeout(id)\n }, [delay, value])\n\n return debounced\n}\n",
9+
"type": "registry:hook"
10+
}
11+
]
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "use-default",
4+
"type": "registry:hook",
5+
"files": [
6+
{
7+
"path": "registry/hooks/use-default/index.ts",
8+
"content": "import { useCallback, useMemo, useState } from \"react\"\n\nimport type { Dispatch, SetStateAction } from \"react\"\n\n/**\n * Manage state but automatically fallback to a default value when the state is `null` or `undefined`.\n *\n * @typeParam T - Type of the state value.\n *\n * @param defaultValue - Default value to use when `initialValue` is `null` or `undefined`.\n * @param initialValue - Initial state.\n * @returns A tuple containing the resolved `value` and a setter function to update the state.\n *\n * @example\n * ```tsx\n * function Profile({ user }: { user?: string | null }) {\n * const { value: name, set: setName } = useDefault(\"Anonymous\", user);\n *\n * return (\n * <div>\n * <p>Hello, {name}!</p>\n * <button onClick={() => setName(null)}>Reset</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useDefault<T>(defaultValue: T, initialValue?: T | (() => T | null | undefined) | null | undefined) {\n const [value, setValue] = useState<T | null | undefined>(initialValue ?? defaultValue)\n\n const memoizedValue = useMemo<T>(() => {\n if (value === null || typeof value === \"undefined\") return defaultValue\n\n return value\n }, [defaultValue, value])\n const memoizedSetValue = useCallback<Dispatch<SetStateAction<T | null | undefined>>>(value => setValue(value), [])\n\n return [memoizedValue, memoizedSetValue] as const\n}\n",
9+
"type": "registry:hook"
10+
}
11+
]
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "use-input-control",
4+
"type": "registry:hook",
5+
"files": [
6+
{
7+
"path": "registry/hooks/use-input-control/index.ts",
8+
"content": "import { useCallback, useMemo, useRef, useState } from \"react\"\n\nimport type { ChangeEvent, FocusEvent } from \"react\"\n\ninterface UseInputValueReturn {\n different: boolean\n dirty: boolean\n touched: boolean\n value: string\n handleBlur: (event: FocusEvent<HTMLInputElement>) => void\n handleChange: (event: ChangeEvent<HTMLInputElement>) => void\n reset: () => void\n}\n\nconst DEFAULT_DIRTY_STATE = false\nconst DEFAULT_TOUCHED_STATE = false\n\n/**\n * Manage a controlled input value and track additional form input states like\n *\n * @param initialValue - Initial value of the input\n * @returns An object with the following properties:\n * - `different`: Whether the value is different from the initial value\n * - `dirty`: Whether the input has been modified at least once\n * - `touched`: Whether the input was focused and blurred\n * - `value`: Current value of the input\n * - `handleBlur`: Function to be called when the input is blurred\n * - `handleChange`: Function that updates the value of the input\n * - `reset` Function to reset the initial value as well as the value of all states\n *\n * @example\n * ```tsx\n * function FormComponent() {\n * const {\n * different,\n * dirty,\n * touched,\n * value,\n * handleBlur,\n * handleChange,\n * reset\n * } = useInputControl(\"John\")\n *\n * return (\n * <form>\n * <fieldset>\n * <input\n * value={value}\n * onChange={handleChange}\n * onBlur={handleBlur}\n * />\n * </fieldset>\n * <p>Dirty: {dirty.toString()}</p>\n * <p>Touched: {touched.toString()}</p>\n * <p>Different: {different.toString()}</p>\n * <button onClick={reset}>Reset</button>\n * </form>\n * )\n * }\n */\nexport function useInputControl(\n initialValue: string,\n): UseInputValueReturn {\n const ref = useRef(initialValue)\n const [value, setValue] = useState(initialValue)\n const [dirty, setDirty] = useState(DEFAULT_DIRTY_STATE)\n const [touched, setTouched] = useState(DEFAULT_TOUCHED_STATE)\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const handleBlur = useCallback((event: FocusEvent<HTMLInputElement>) => {\n setTouched(true)\n }, [])\n const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {\n setDirty(true)\n setValue(event.currentTarget.value)\n }, [])\n const reset = useCallback(() => {\n setDirty(DEFAULT_DIRTY_STATE)\n setTouched(DEFAULT_TOUCHED_STATE)\n setValue(ref.current)\n }, [])\n\n const different = ref.current !== value\n\n return useMemo(\n () => ({\n different,\n dirty,\n touched,\n value,\n handleBlur,\n handleChange,\n reset,\n }),\n [different, dirty, touched, value, handleBlur, handleChange, reset],\n )\n}\n",
9+
"type": "registry:hook"
10+
}
11+
]
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "use-media-query",
4+
"type": "registry:hook",
5+
"files": [
6+
{
7+
"path": "registry/hooks/use-media-query/index.ts",
8+
"content": "import { useCallback, useSyncExternalStore } from \"react\"\n\n/**\n * Subscribe and respond to media query changes.\n *\n * @param `query` - Media query to match. It must be a [valid CSS media query string](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries)\n * @returns Boolean indicating whether the media query currently matches\n *\n * @example\n * ```tsx\n * function Component() {\n * const isMobile = useMediaQuery(\"(max-width: 768px)\")\n *\n * return (\n * <div>\n * {isMobile ? \"Viewing on mobile\" : \"Viewing on desktop\"}\n * </div>\n * )\n * }\n * ```\n *\n * @example\n * ```tsx\n * function DarkModeIndicator() {\n * const prefersDark = useMediaQuery(\"(prefers-color-scheme: dark)\")\n *\n * return (\n * <span>{prefersDark ? \"Dark mode\" : \"Light mode\"}</span>\n * )\n * }\n * ```\n */\nexport function useMediaQuery(query: string): boolean {\n const subscribe = useCallback((callback: () => void) => {\n const match = window.matchMedia(query)\n\n match.addEventListener(\"change\", callback)\n\n return () => match.removeEventListener(\"change\", callback)\n }, [query])\n\n const getSnapshot = () => {\n return window.matchMedia(query).matches\n }\n\n const getServerSnapshot = () => {\n throw Error(\"useMatchMedia is a client-only hook\")\n }\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n}\n",
9+
"type": "registry:hook"
10+
}
11+
]
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "use-set",
4+
"type": "registry:hook",
5+
"files": [
6+
{
7+
"path": "registry/hooks/use-set/index.ts",
8+
"content": "import { useCallback, useRef, useState } from \"react\"\n\nexport interface UseSetReturn<T> {\n set: Readonly<Set<T>>\n add: (key: T) => void\n clear: () => void\n remove: (key: T) => void\n reset: () => void\n toggle: (key: T) => void\n}\n\n/**\n * Manage a JavaScript `Set` of items with additional utility methods.\n *\n * @typeParam T - Type of values stored in the `Set`\n *\n * @param `initialState` - Optional iterable used to initialize the `Set`. If omitted, the set starts empty.\n * @returns An object with the following properties:\n * - `set`: Current set of items\n * - `add`: Function that adds an item to the set\n * - `clear`: Function that removes all items from the set\n * - `remove`: Function that removes an from the set\n * - `reset`: Function that resets the set back to `initialState`\n * - `toggle`: Adds the item if it doesn’t exist, or removes it if it does\n *\n * @example\n * ```tsx\n * function TagsSelector() {\n * const { set, add, remove, toggle, reset, clear } = useSet<string>([\"react\", \"typescript\"])\n *\n * return (\n * <div>\n * <button onClick={() => toggle(\"react\")}>Toggle React</button>\n * <button onClick={() => toggle(\"vue\")}>Toggle Vue</button>\n * <button onClick={reset}>Reset</button>\n * <button onClick={clear}>Clear</button>\n *\n * <ul>\n * {[...set].map(tag => <li key={tag}>{tag}</li>)}\n * </ul>\n * </div>\n * )\n * }\n * ```\n */\nexport function useSet<T>(\n initialState = new Set<T>(),\n): UseSetReturn<T> {\n const ref = useRef(initialState)\n const [set, setSet] = useState<Set<T>>(ref.current)\n\n const add = useCallback((item: T) => {\n setSet(prev => new Set(prev).add(item))\n }, [])\n\n const clear = useCallback(() => {\n setSet(new Set())\n }, [])\n\n const remove = useCallback((item: T) => {\n setSet((prev) => {\n const next = new Set(prev)\n next.delete(item)\n return next\n })\n }, [])\n\n const reset = useCallback(() => {\n setSet(ref.current)\n }, [])\n\n const toggle = useCallback((item: T) => {\n setSet((prev) => {\n const next = new Set(prev)\n if (next.has(item)) {\n next.delete(item)\n }\n else {\n next.add(item)\n }\n\n return next\n })\n }, [])\n\n return {\n set,\n add,\n clear,\n remove,\n reset,\n toggle,\n }\n}\n",
9+
"type": "registry:hook"
10+
}
11+
]
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "use-timeout",
4+
"type": "registry:hook",
5+
"files": [
6+
{
7+
"path": "registry/hooks/use-timeout/index.ts",
8+
"content": "import { useEffect, useRef } from \"react\"\n\n/**\n * Invoke a callback function after a specified delay.\n *\n * The latest version of the callback is always invoked, even if it changes between renders. If the delay changes, the previous timeout is cancelled and a new one is scheduled. If `delay` is `null` or `undefined`, the timeout is paused.\n *\n * @param `callback` - Function to call after the timeout\n * @param `delay` - Delay in milliseconds before invoking the callback. If `null` or `undefined`, the timeout is paused.\n *\n * @example\n * ```tsx\n * function Component() {\n * const [count, setCount] = useState(0)\n *\n * useTimeout(() => {\n * console.log(\"Timeout fired! Count:\", count)\n * }, 1000)\n *\n * return (\n * <button onClick={() => setCount(c => c + 1)}>\n * Increment count\n * </button>\n * )\n * }\n * ```\n */\nexport function useTimeout(callback: () => void, delay: number | null | undefined) {\n const ref = useRef(callback)\n ref.current = callback\n\n useEffect(() => {\n if (delay === null || delay === undefined) return\n\n const id = setTimeout(() => {\n ref.current()\n }, delay)\n\n return () => clearTimeout(id)\n }, [delay])\n}\n",
9+
"type": "registry:hook"
10+
}
11+
]
12+
}

0 commit comments

Comments
 (0)