@umituz/web-design-system 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/src/domain/tokens/animation.tokens.ts +87 -0
  3. package/src/domain/tokens/index.ts +11 -0
  4. package/src/presentation/atoms/Checkbox.tsx +50 -0
  5. package/src/presentation/atoms/Divider.tsx +34 -0
  6. package/src/presentation/atoms/Link.tsx +45 -0
  7. package/src/presentation/atoms/Progress.tsx +48 -0
  8. package/src/presentation/atoms/Radio.tsx +42 -0
  9. package/src/presentation/atoms/Skeleton.tsx +38 -0
  10. package/src/presentation/atoms/Slider.tsx +49 -0
  11. package/src/presentation/atoms/Tooltip.tsx +76 -0
  12. package/src/presentation/atoms/index.ts +24 -0
  13. package/src/presentation/hooks/index.ts +13 -0
  14. package/src/presentation/hooks/useClickOutside.ts +33 -0
  15. package/src/presentation/hooks/useClipboard.ts +38 -0
  16. package/src/presentation/hooks/useDebounce.ts +22 -0
  17. package/src/presentation/hooks/useKeyboard.ts +89 -0
  18. package/src/presentation/hooks/useScrollLock.ts +26 -0
  19. package/src/presentation/hooks/useToggle.ts +16 -0
  20. package/src/presentation/molecules/CheckboxGroup.tsx +68 -0
  21. package/src/presentation/molecules/InputGroup.tsx +65 -0
  22. package/src/presentation/molecules/RadioGroup.tsx +63 -0
  23. package/src/presentation/molecules/Select.tsx +41 -0
  24. package/src/presentation/molecules/Textarea.tsx +43 -0
  25. package/src/presentation/molecules/index.ts +15 -0
  26. package/src/presentation/organisms/Accordion.tsx +117 -0
  27. package/src/presentation/organisms/Breadcrumb.tsx +83 -0
  28. package/src/presentation/organisms/Table.tsx +120 -0
  29. package/src/presentation/organisms/Tabs.tsx +99 -0
  30. package/src/presentation/organisms/index.ts +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-design-system",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Web Design System - Atomic Design components (Atoms, Molecules, Organisms) for React applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Animation Tokens
3
+ * @description Animation durations, easings, and keyframes
4
+ */
5
+
6
+ export type DurationToken =
7
+ | 'instant'
8
+ | 'fast'
9
+ | 'normal'
10
+ | 'slow'
11
+ | 'slower';
12
+
13
+ export type EasingToken =
14
+ | 'linear'
15
+ | 'ease'
16
+ | 'easeIn'
17
+ | 'easeOut'
18
+ | 'easeInOut'
19
+ | 'bounce'
20
+ | 'elastic';
21
+
22
+ export const durations: Record<DurationToken, string> = {
23
+ instant: '150ms',
24
+ fast: '200ms',
25
+ normal: '300ms',
26
+ slow: '500ms',
27
+ slower: '700ms',
28
+ };
29
+
30
+ export const easings: Record<EasingToken, string> = {
31
+ linear: 'linear',
32
+ ease: 'cubic-bezier(0.4, 0, 0.2, 1)',
33
+ easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
34
+ easeOut: 'cubic-bezier(0, 0, 0.2, 1)',
35
+ easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
36
+ bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
37
+ elastic: 'cubic-bezier(0.87, 0, 0.13, 1)',
38
+ };
39
+
40
+ export const animations = {
41
+ fade: 'fade 300ms ease-out',
42
+ slide: 'slide 300ms ease-out',
43
+ scale: 'scale 200ms ease-out',
44
+ spin: 'spin 1s linear infinite',
45
+ ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
46
+ pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
47
+ bounce: 'bounce 1s infinite',
48
+ };
49
+
50
+ export const keyframes = {
51
+ fade: {
52
+ from: { opacity: '0' },
53
+ to: { opacity: '1' },
54
+ },
55
+ slide: {
56
+ from: { transform: 'translateY(-10px)', opacity: '0' },
57
+ to: { transform: 'translateY(0)', opacity: '1' },
58
+ },
59
+ scale: {
60
+ from: { transform: 'scale(0.95)', opacity: '0' },
61
+ to: { transform: 'scale(1)', opacity: '1' },
62
+ },
63
+ spin: {
64
+ from: { transform: 'rotate(0deg)' },
65
+ to: { transform: 'rotate(360deg)' },
66
+ },
67
+ ping: {
68
+ '75%, 100%': {
69
+ transform: 'scale(2)',
70
+ opacity: '0',
71
+ },
72
+ },
73
+ pulse: {
74
+ '0%, 100%': { opacity: '1' },
75
+ '50%': { opacity: '0.5' },
76
+ },
77
+ bounce: {
78
+ '0%, 100%': {
79
+ transform: 'translateY(-25%)',
80
+ animationTimingFunction: 'cubic-bezier(0.8, 0, 1, 1)',
81
+ },
82
+ '50%': {
83
+ transform: 'translateY(0)',
84
+ animationTimingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
85
+ },
86
+ },
87
+ };
@@ -42,3 +42,14 @@ export type {
42
42
  export {
43
43
  shadows,
44
44
  } from './shadow.tokens';
45
+
46
+ export type {
47
+ DurationToken,
48
+ EasingToken,
49
+ } from './animation.tokens';
50
+ export {
51
+ durations,
52
+ easings,
53
+ animations,
54
+ keyframes,
55
+ } from './animation.tokens';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Checkbox Component (Atom)
3
+ * @description Checkbox input element
4
+ */
5
+
6
+ import { forwardRef, type InputHTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+
10
+ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, BaseProps {
11
+ checked?: boolean;
12
+ onCheckedChange?: (checked: boolean) => void;
13
+ size?: 'sm' | 'md' | 'lg';
14
+ }
15
+
16
+ const sizeStyles = {
17
+ sm: 'h-4 w-4',
18
+ md: 'h-5 w-5',
19
+ lg: 'h-6 w-6',
20
+ };
21
+
22
+ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
23
+ ({ className, checked, onCheckedChange, size = 'md', disabled, ...props }, ref) => {
24
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
25
+ onCheckedChange?.(e.target.checked);
26
+ props.onChange?.(e);
27
+ };
28
+
29
+ return (
30
+ <input
31
+ ref={ref}
32
+ type="checkbox"
33
+ checked={checked}
34
+ onChange={handleChange}
35
+ disabled={disabled}
36
+ className={cn(
37
+ 'shrink-0 rounded border border-primary ring-offset-background',
38
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
39
+ 'disabled:cursor-not-allowed disabled:opacity-50',
40
+ 'accent-primary',
41
+ sizeStyles[size],
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+ );
49
+
50
+ Checkbox.displayName = 'Checkbox';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Divider Component (Atom)
3
+ * @description Visual separator line
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+
10
+ export interface DividerProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
11
+ orientation?: 'horizontal' | 'vertical';
12
+ decorative?: boolean;
13
+ }
14
+
15
+ const orientationStyles = {
16
+ horizontal: 'h-px w-full',
17
+ vertical: 'h-full w-px',
18
+ };
19
+
20
+ export const Divider = forwardRef<HTMLDivElement, DividerProps>(
21
+ ({ className, orientation = 'horizontal', decorative = true, role = 'separator', ...props }, ref) => {
22
+ return (
23
+ <div
24
+ ref={ref}
25
+ role={decorative ? 'none' : role}
26
+ aria-orientation={orientation}
27
+ className={cn('shrink-0 bg-border', orientationStyles[orientation], className)}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+ );
33
+
34
+ Divider.displayName = 'Divider';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Link Component (Atom)
3
+ * @description Styled anchor/link element
4
+ */
5
+
6
+ import { forwardRef, type AnchorHTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+
10
+ export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement>, BaseProps {
11
+ variant?: 'default' | 'primary' | 'muted';
12
+ underline?: 'none' | 'hover' | 'always';
13
+ }
14
+
15
+ const variantStyles: Record<'default' | 'primary' | 'muted', string> = {
16
+ default: 'text-foreground',
17
+ primary: 'text-primary',
18
+ muted: 'text-muted-foreground',
19
+ };
20
+
21
+ const underlineStyles: Record<'none' | 'hover' | 'always', string> = {
22
+ none: '',
23
+ hover: 'hover:underline',
24
+ always: 'underline',
25
+ };
26
+
27
+ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
28
+ ({ className, variant = 'default', underline = 'hover', ...props }, ref) => {
29
+ return (
30
+ <a
31
+ ref={ref}
32
+ className={cn(
33
+ 'inline-flex items-center gap-1 transition-colors',
34
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm',
35
+ variantStyles[variant],
36
+ underlineStyles[underline],
37
+ className
38
+ )}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+ );
44
+
45
+ Link.displayName = 'Link';
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Progress Component (Atom)
3
+ * @description Progress indicator bar
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+
10
+ export interface ProgressProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
11
+ value?: number;
12
+ max?: number;
13
+ size?: 'sm' | 'md' | 'lg';
14
+ }
15
+
16
+ const sizeStyles: Record<'sm' | 'md' | 'lg', string> = {
17
+ sm: 'h-1',
18
+ md: 'h-2',
19
+ lg: 'h-3',
20
+ };
21
+
22
+ export const Progress = forwardRef<HTMLDivElement, ProgressProps>(
23
+ ({ className, value = 0, max = 100, size = 'md', ...props }, ref) => {
24
+ const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
25
+
26
+ return (
27
+ <div
28
+ ref={ref}
29
+ role="progressbar"
30
+ aria-valuenow={value}
31
+ aria-valuemax={max}
32
+ className={cn(
33
+ 'w-full bg-muted rounded-full overflow-hidden',
34
+ sizeStyles[size],
35
+ className
36
+ )}
37
+ {...props}
38
+ >
39
+ <div
40
+ className="h-full bg-primary transition-all duration-300 ease-in-out"
41
+ style={{ width: `${percentage}%` }}
42
+ />
43
+ </div>
44
+ );
45
+ }
46
+ );
47
+
48
+ Progress.displayName = 'Progress';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Radio Component (Atom)
3
+ * @description Radio input element
4
+ */
5
+
6
+ import { forwardRef, type InputHTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+
10
+ export interface RadioProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, BaseProps {
11
+ value: string;
12
+ size?: 'sm' | 'md' | 'lg';
13
+ }
14
+
15
+ const sizeStyles = {
16
+ sm: 'h-4 w-4',
17
+ md: 'h-5 w-5',
18
+ lg: 'h-6 w-6',
19
+ };
20
+
21
+ export const Radio = forwardRef<HTMLInputElement, RadioProps>(
22
+ ({ className, size = 'md', disabled, ...props }, ref) => {
23
+ return (
24
+ <input
25
+ ref={ref}
26
+ type="radio"
27
+ disabled={disabled}
28
+ className={cn(
29
+ 'shrink-0 rounded-full border border-primary ring-offset-background',
30
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
31
+ 'disabled:cursor-not-allowed disabled:opacity-50',
32
+ 'accent-primary',
33
+ sizeStyles[size],
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+ );
41
+
42
+ Radio.displayName = 'Radio';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Skeleton Component (Atom)
3
+ * @description Loading placeholder
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+
10
+ export interface SkeletonProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
11
+ variant?: 'text' | 'circular' | 'rectangular';
12
+ }
13
+
14
+ const variantStyles: Record<'text' | 'circular' | 'rectangular', string> = {
15
+ text: 'h-4 w-full rounded',
16
+ circular: 'h-12 w-12 rounded-full',
17
+ rectangular: 'h-24 w-full rounded',
18
+ };
19
+
20
+ export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
21
+ ({ className, variant = 'rectangular', ...props }, ref) => {
22
+ return (
23
+ <div
24
+ ref={ref}
25
+ className={cn(
26
+ 'animate-pulse bg-muted',
27
+ variantStyles[variant],
28
+ className
29
+ )}
30
+ role="status"
31
+ aria-label="Loading"
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+ );
37
+
38
+ Skeleton.displayName = 'Skeleton';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Slider Component (Atom)
3
+ * @description Range slider input
4
+ */
5
+
6
+ import { forwardRef, type InputHTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+
10
+ export interface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, BaseProps {
11
+ min?: number;
12
+ max?: number;
13
+ step?: number;
14
+ value?: number[];
15
+ onValueChange?: (value: number[]) => void;
16
+ }
17
+
18
+ export const Slider = forwardRef<HTMLInputElement, SliderProps>(
19
+ ({ className, min = 0, max = 100, step = 1, value = [0], onValueChange, disabled, ...props }, ref) => {
20
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
21
+ const newValue = [Number(e.target.value)];
22
+ onValueChange?.(newValue);
23
+ props.onChange?.(e);
24
+ };
25
+
26
+ return (
27
+ <input
28
+ ref={ref}
29
+ type="range"
30
+ min={min}
31
+ max={max}
32
+ step={step}
33
+ value={value[0]}
34
+ onChange={handleChange}
35
+ disabled={disabled}
36
+ className={cn(
37
+ 'w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer',
38
+ 'accent-primary',
39
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
40
+ 'disabled:cursor-not-allowed disabled:opacity-50',
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+ );
48
+
49
+ Slider.displayName = 'Slider';
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Tooltip Component (Atom)
3
+ * @description Popup with additional information
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes, type ReactNode, useState, useRef, useEffect } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps, ChildrenProps } from '../../domain/types';
9
+
10
+ export interface TooltipProps extends HTMLAttributes<HTMLDivElement>, BaseProps, ChildrenProps {
11
+ content: ReactNode;
12
+ placement?: 'top' | 'bottom' | 'left' | 'right';
13
+ delay?: number;
14
+ }
15
+
16
+ const placementStyles: Record<'top' | 'bottom' | 'left' | 'right', string> = {
17
+ top: 'bottom-full mb-2',
18
+ bottom: 'top-full mt-2',
19
+ left: 'right-full mr-2',
20
+ right: 'left-full ml-2',
21
+ };
22
+
23
+ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
24
+ ({ className, children, content, placement = 'top', delay = 200, ...props }, ref) => {
25
+ const [isOpen, setIsOpen] = useState(false);
26
+ const timeoutRef = useRef<NodeJS.Timeout>();
27
+
28
+ const handleMouseEnter = () => {
29
+ timeoutRef.current = setTimeout(() => {
30
+ setIsOpen(true);
31
+ }, delay);
32
+ };
33
+
34
+ const handleMouseLeave = () => {
35
+ if (timeoutRef.current) {
36
+ clearTimeout(timeoutRef.current);
37
+ }
38
+ setIsOpen(false);
39
+ };
40
+
41
+ useEffect(() => {
42
+ return () => {
43
+ if (timeoutRef.current) {
44
+ clearTimeout(timeoutRef.current);
45
+ }
46
+ };
47
+ }, []);
48
+
49
+ return (
50
+ <div
51
+ ref={ref}
52
+ className="relative inline-block"
53
+ onMouseEnter={handleMouseEnter}
54
+ onMouseLeave={handleMouseLeave}
55
+ {...props}
56
+ >
57
+ {children}
58
+ {isOpen && (
59
+ <div
60
+ className={cn(
61
+ 'absolute z-50 px-2 py-1 text-xs text-white bg-foreground rounded shadow-lg whitespace-nowrap',
62
+ placementStyles[placement],
63
+ 'animate-in fade-in-0 zoom-in-95',
64
+ className
65
+ )}
66
+ role="tooltip"
67
+ >
68
+ {content}
69
+ </div>
70
+ )}
71
+ </div>
72
+ );
73
+ }
74
+ );
75
+
76
+ Tooltip.displayName = 'Tooltip';
@@ -21,3 +21,27 @@ export type { IconProps } from './Icon';
21
21
 
22
22
  export { Spinner } from './Spinner';
23
23
  export type { SpinnerProps } from './Spinner';
24
+
25
+ export { Checkbox } from './Checkbox';
26
+ export type { CheckboxProps } from './Checkbox';
27
+
28
+ export { Radio } from './Radio';
29
+ export type { RadioProps } from './Radio';
30
+
31
+ export { Slider } from './Slider';
32
+ export type { SliderProps } from './Slider';
33
+
34
+ export { Divider } from './Divider';
35
+ export type { DividerProps } from './Divider';
36
+
37
+ export { Skeleton } from './Skeleton';
38
+ export type { SkeletonProps } from './Skeleton';
39
+
40
+ export { Link } from './Link';
41
+ export type { LinkProps } from './Link';
42
+
43
+ export { Tooltip } from './Tooltip';
44
+ export type { TooltipProps } from './Tooltip';
45
+
46
+ export { Progress } from './Progress';
47
+ export type { ProgressProps } from './Progress';
@@ -11,3 +11,16 @@ export { useMediaQuery, useBreakpoint } from './useMediaQuery';
11
11
  export type { Breakpoint } from './useMediaQuery';
12
12
 
13
13
  export { useLocalStorage } from './useLocalStorage';
14
+
15
+ export { useClickOutside } from './useClickOutside';
16
+
17
+ export { useKeyboard, useEscape } from './useKeyboard';
18
+ export type { KeyboardKey, KeyboardModifier, KeyboardOptions, UseClipboardReturn } from './useKeyboard';
19
+
20
+ export { useDebounce } from './useDebounce';
21
+
22
+ export { useClipboard } from './useClipboard';
23
+
24
+ export { useToggle } from './useToggle';
25
+
26
+ export { useScrollLock } from './useScrollLock';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * useClickOutside Hook
3
+ * @description Detect clicks outside an element
4
+ */
5
+
6
+ import { useEffect, useRef } from 'react';
7
+
8
+ export function useClickOutside<T extends HTMLElement>(
9
+ callback: () => void,
10
+ enabled: boolean = true
11
+ ) {
12
+ const ref = useRef<T>(null);
13
+
14
+ useEffect(() => {
15
+ if (!enabled) return;
16
+
17
+ const handleClick = (event: MouseEvent) => {
18
+ if (ref.current && !ref.current.contains(event.target as Node)) {
19
+ callback();
20
+ }
21
+ };
22
+
23
+ document.addEventListener('mousedown', handleClick);
24
+ document.addEventListener('touchstart', handleClick);
25
+
26
+ return () => {
27
+ document.removeEventListener('mousedown', handleClick);
28
+ document.removeEventListener('touchstart', handleClick);
29
+ };
30
+ }, [callback, enabled]);
31
+
32
+ return ref;
33
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * useClipboard Hook
3
+ * @description Clipboard operations
4
+ */
5
+
6
+ import { useState, useCallback } from 'react';
7
+
8
+ export interface UseClipboardReturn {
9
+ value: string;
10
+ copied: boolean;
11
+ copy: (text: string) => Promise<void>;
12
+ }
13
+
14
+ export function useClipboard(): UseClipboardReturn {
15
+ const [value, setValue] = useState<string>('');
16
+ const [copied, setCopied] = useState<boolean>(false);
17
+
18
+ const copy = useCallback(async (text: string) => {
19
+ try {
20
+ await navigator.clipboard.writeText(text);
21
+ setValue(text);
22
+ setCopied(true);
23
+
24
+ setTimeout(() => {
25
+ setCopied(false);
26
+ }, 2000);
27
+ } catch (error) {
28
+ console.error('Failed to copy:', error);
29
+ setCopied(false);
30
+ }
31
+ }, []);
32
+
33
+ return {
34
+ value,
35
+ copied,
36
+ copy,
37
+ };
38
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * useDebounce Hook
3
+ * @description Debounce a value
4
+ */
5
+
6
+ import { useState, useEffect } from 'react';
7
+
8
+ export function useDebounce<T>(value: T, delay: number = 500): T {
9
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
10
+
11
+ useEffect(() => {
12
+ const handler = setTimeout(() => {
13
+ setDebouncedValue(value);
14
+ }, delay);
15
+
16
+ return () => {
17
+ clearTimeout(handler);
18
+ };
19
+ }, [value, delay]);
20
+
21
+ return debouncedValue;
22
+ }