diff --git a/src/components/ui/LoadMoreButton.test.tsx b/src/components/ui/LoadMoreButton.test.tsx
new file mode 100644
index 0000000..14c1024
--- /dev/null
+++ b/src/components/ui/LoadMoreButton.test.tsx
@@ -0,0 +1,150 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import { LoadMoreButton } from './LoadMoreButton'
+
+describe('LoadMoreButton', () => {
+ it('renders button when hasNextPage is true', () => {
+ const mockOnLoadMore = vi.fn()
+ render(
+
+ )
+
+ const button = screen.getByRole('button', { name: 'Load more results' })
+ expect(button).toBeTruthy()
+ })
+
+ it('hides button when hasNextPage is false', () => {
+ const mockOnLoadMore = vi.fn()
+ const { container } = render(
+
+ )
+
+ const button = screen.queryByRole('button', { name: 'Load more results' })
+ expect(button).toBeNull()
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('shows spinner when isFetchingNextPage is true', () => {
+ const mockOnLoadMore = vi.fn()
+ render(
+
+ )
+
+ const button = screen.getByRole('button', { name: 'Load more results' })
+ expect(button).toBeTruthy()
+
+ // Check that the spinner SVG is present
+ const spinner = button.querySelector('svg')
+ expect(spinner).toBeTruthy()
+ expect(spinner?.className).toContain('animate-spin')
+
+ // Check that "Loading..." text is shown
+ expect(screen.getByText('Loading...')).toBeTruthy()
+ })
+
+ it('shows "Load More" text when not fetching', () => {
+ const mockOnLoadMore = vi.fn()
+ render(
+
+ )
+
+ expect(screen.getByText('Load More')).toBeTruthy()
+ })
+
+ it('disables button when isFetchingNextPage is true', () => {
+ const mockOnLoadMore = vi.fn()
+ render(
+
+ )
+
+ const button = screen.getByRole('button', { name: 'Load more results' })
+ expect(button).toBeDisabled()
+ })
+
+ it('calls onLoadMore when clicked', () => {
+ const mockOnLoadMore = vi.fn()
+
+ render(
+
+ )
+
+ const button = screen.getByRole('button', { name: 'Load more results' })
+ fireEvent.click(button)
+
+ expect(mockOnLoadMore).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not call onLoadMore when disabled by isFetchingNextPage', () => {
+ const mockOnLoadMore = vi.fn()
+
+ render(
+
+ )
+
+ const button = screen.getByRole('button', { name: 'Load more results' })
+ fireEvent.click(button)
+
+ expect(mockOnLoadMore).not.toHaveBeenCalled()
+ })
+
+ it('has correct aria-label for accessibility', () => {
+ const mockOnLoadMore = vi.fn()
+ render(
+
+ )
+
+ const button = screen.getByRole('button', { name: 'Load more results' })
+ expect(button).toHaveAttribute('aria-label', 'Load more results')
+ })
+
+ it('respects disabled prop', () => {
+ const mockOnLoadMore = vi.fn()
+
+ render(
+
+ )
+
+ const button = screen.getByRole('button', { name: 'Load more results' })
+ expect(button).toBeDisabled()
+
+ fireEvent.click(button)
+ expect(mockOnLoadMore).not.toHaveBeenCalled()
+ })
+})
diff --git a/src/components/ui/LoadMoreButton.tsx b/src/components/ui/LoadMoreButton.tsx
new file mode 100644
index 0000000..17e8b92
--- /dev/null
+++ b/src/components/ui/LoadMoreButton.tsx
@@ -0,0 +1,52 @@
+// ─── Reusable: LoadMoreButton ─────────────────────────────────────────────────
+
+interface LoadMoreButtonProps {
+ hasNextPage: boolean;
+ isFetchingNextPage: boolean;
+ onLoadMore: () => void;
+ disabled?: boolean;
+}
+
+export function LoadMoreButton({
+ hasNextPage,
+ isFetchingNextPage,
+ onLoadMore,
+ disabled = false,
+}: LoadMoreButtonProps) {
+ // Hide button when there's no next page
+ if (!hasNextPage) {
+ return null;
+ }
+
+ return (
+
+ );
+}