npm package: @meadown/scroll-progress-trigger
Description: A scroll progress bar trigger for React/Next.js
Author: Dewan Meadown
Latest version: 1.1.7

# @meadown/scroll-progress-trigger

A powerful, lightweight React/Next.js library for creating scroll-controlled experiences with precise block-based navigation. Perfect for image galleries, story viewers, product tours, and any scroll-driven UI.

## Features

- 🎯 **Trigger-based activation** - Progress only advances when hovering/touching specific elements
- 📱 **Touch & Mouse support** - Works seamlessly on desktop and mobile devices
- 🧠 **Smart scroll direction** - Automatically adapts to user's natural scroll preferences
- 🎛️ **Block-based navigation** - Navigate arrays without skipping items - solved the common "one scroll jumps multiple items" problem
- 🔄 **Smooth boundaries** - Never get stuck at 0% or 100%, seamlessly transition between sections
- ⏸️ **Flexible hold behavior** - Choose between holding progress or auto-decreasing when idle
- 🪶 **Lightweight** - Zero dependencies (except React), efficient implementation
- 🎨 **Fully customizable** - Complete control over behavior and styling
- 📝 **Full TypeScript support** - Complete type definitions with IntelliSense support

## Installation

```bash
npm install @meadown/scroll-progress-trigger
```

## Quick Start

### Basic Progress Bar

```tsx
import { useRef } from "react"
import { useScrollProgress } from "@meadown/scroll-progress-trigger"

function ProgressBar() {
  const triggerRef = useRef<HTMLDivElement>(null)

  const { progress, isActive } = useScrollProgress({
    triggerRef,
    scrollDuration: 3000,
    onComplete: () => console.log("Completed!")
  })

  return (
    <div ref={triggerRef} style={{ padding: "20px" }}>
      <div style={{
        width: "100%",
        height: "4px",
        background: "#f0f0f0",
        borderRadius: "2px"
      }}>
        <div style={{
          width: `${progress}%`,
          height: "100%",
          background: "#3b82f6",
          transition: "width 0.1s ease-out"
        }} />
      </div>
      <p>{isActive ? "Scrolling..." : "Hover and scroll"} - {progress}%</p>
    </div>
  )
}
```

### Array Navigation (Solving the "Skipping Items" Problem)

**Problem**: With standard progress bars, one scroll event can jump multiple array items.
**Solution**: Use `totalBlocks` with `scrollsPerBlock` for precise control.

```tsx
import { useRef } from "react"
import { useScrollProgress } from "@meadown/scroll-progress-trigger"

function ImageGallery() {
  const triggerRef = useRef<HTMLDivElement>(null)
  const images = [
    "photo1.jpg",
    "photo2.jpg",
    "photo3.jpg",
    "photo4.jpg",
    "photo5.jpg"
  ]

  const { currentIndex, blockProgress } = useScrollProgress({
    triggerRef,
    totalBlocks: images.length,
    scrollsPerBlock: 3, // Requires 3 scroll events to move to next image
    onIndexChange: (current, previous) => {
      console.log(`Changed from image ${previous} to ${current}`)
    }
  })

  return (
    <div ref={triggerRef} style={{ position: "relative", height: "500px" }}>
      <img
        src={images[currentIndex!]}
        alt={`Photo ${currentIndex! + 1}`}
        style={{
          width: "100%",
          height: "100%",
          objectFit: "cover",
          opacity: (100 - blockProgress!) / 100 + 0.5 // Fade effect
        }}
      />
      <div style={{ position: "absolute", bottom: 20, left: 20, color: "white" }}>
        Image {currentIndex! + 1} of {images.length}
      </div>
    </div>
  )
}
```

## API

### useScrollProgress Options

| Option               | Type                                          | Default     | Description                                            |
| -------------------- | --------------------------------------------- | ----------- | ------------------------------------------------------ |
| `onComplete`         | `() => void`                                  | `undefined` | Callback fired when progress reaches 100%              |
| `scrollDuration`     | `number`                                      | `3000`      | Duration in milliseconds for full progress             |
| `triggerRef`         | `React.MutableRefObject<HTMLElement \| null>` | `undefined` | Element that triggers progress on hover/touch          |
| `scrollDirection`    | `'natural' \| 'inverted' \| 'system'`         | `'system'`  | Scroll direction behavior (auto-detects when 'system') |
| `isAutoHoldProgress` | `boolean`                                     | `true`      | Whether progress holds when not scrolling vs auto-decreases |
| `totalBlocks`        | `number`                                      | `undefined` | Divide progress into discrete blocks/indices           |
| `scrollsPerBlock`    | `number`                                      | `1`         | Number of scroll events required to advance one block  |
| `snapToBlocks`       | `boolean`                                     | `false`     | Snap progress to exact block boundaries                |
| `onIndexChange`      | `(current: number, previous: number) => void` | `undefined` | Callback fired when block index changes                |
| `onBlockProgress`    | `(index: number, progress: number) => void`   | `undefined` | Callback fired on scroll with current block progress   |

### Return Values

| Value           | Type         | Description                                   |
| --------------- | ------------ | --------------------------------------------- |
| `progress`      | `number`     | Current progress value (0-100)                |
| `resetProgress` | `() => void` | Function to reset progress to 0               |
| `isActive`      | `boolean`    | Whether scroll progress is currently active   |
| `currentIndex`  | `number`     | Current block index (when totalBlocks is set) |
| `previousIndex` | `number`     | Previous block index (when totalBlocks is set)|
| `blockProgress` | `number`     | Progress within current block 0-100           |

## Scroll Direction Behavior

The library automatically adapts to your natural scroll preferences:

- **`'system'` (default)**: Automatically detects your scroll direction preference within the first 5 scroll interactions
- **`'natural'`**: Scroll down = increase progress (like iOS/macOS natural scrolling)
- **`'inverted'`**: Scroll down = decrease progress (traditional scrolling)

### Auto-Detection Process

When using `scrollDirection: 'system'`, the library:

1. Collects your first 5 scroll interactions
2. Analyzes your expected vs actual behavior patterns
3. Automatically adapts to match your natural scrolling preference
4. Falls back to device heuristics if needed

```tsx
const { progress } = useScrollProgress({
  scrollDirection: "system" // Auto-detects your preference
  // scrollDirection: 'natural', // Force natural scrolling
  // scrollDirection: 'inverted', // Force traditional scrolling
})
```

## Progress Hold Behavior

Control what happens when you stop scrolling:

### Auto-Hold Progress (default)
- **`isAutoHoldProgress: true`**: Progress stays at current value when you stop scrolling
- Perfect for scenarios where you want users to maintain their progress position

```tsx
const { progress } = useScrollProgress({
  isAutoHoldProgress: true, // Progress stays at 50% if you stop scrolling at 50%
})
```

### Auto-Decrease Progress
- **`isAutoHoldProgress: false`**: Progress automatically decreases toward 0 when you stop scrolling
- Ideal for creating urgency or automatic reset behavior

```tsx
const { progress } = useScrollProgress({
  isAutoHoldProgress: false, // Progress drops from 50% → 0 when you stop scrolling
})
```

### Auto-Decrease Details
When `isAutoHoldProgress: false`:
- ⏱️ **500ms delay** before auto-decrease starts
- 📉 **Smooth animation** at the same rate as scroll interaction
- 🛑 **Instant interruption** when user scrolls again
- 🎯 **Only when active** - only decreases while hovering/touching the trigger element

## Block-Based Navigation (Array Support)

Perfect for navigating through arrays of items without skipping! This solves the common problem where one scroll event jumps multiple items.

### Basic Array Navigation

```tsx
import React, { useRef } from "react"
import { useScrollProgress } from "@meadown/scroll-progress-trigger"

function ImageGallery() {
  const triggerRef = useRef<HTMLDivElement>(null)
  const images = ['img1.jpg', 'img2.jpg', 'img3.jpg', 'img4.jpg', 'img5.jpg']

  const { currentIndex } = useScrollProgress({
    totalBlocks: images.length,
    scrollsPerBlock: 3, // Requires 3 scrolls to move to next image
    triggerRef,
    onIndexChange: (current, previous) => {
      console.log(`Moved from image ${previous} to ${current}`)
    }
  })

  return (
    <div ref={triggerRef}>
      <img src={images[currentIndex]} alt={`Image ${currentIndex + 1}`} />
      <p>Image {currentIndex + 1} of {images.length}</p>
    </div>
  )
}
```

### Story/Slide Experience

```tsx
const stories = [
  { title: "Intro", content: "..." },
  { title: "Chapter 1", content: "..." },
  { title: "Chapter 2", content: "..." },
  { title: "Conclusion", content: "..." }
]

const { currentIndex, blockProgress } = useScrollProgress({
  totalBlocks: stories.length,
  snapToBlocks: true, // Clean snap to each story
  scrollsPerBlock: 2,
  onIndexChange: (index) => {
    // Trigger animations when story changes
    animateStoryEntry(stories[index])
  }
})

return (
  <div ref={triggerRef}>
    <h1>{stories[currentIndex].title}</h1>
    <p>{stories[currentIndex].content}</p>
    {/* Use blockProgress for within-story animations */}
    <div style={{ opacity: blockProgress / 100 }}>
      Additional content fades in as you scroll through the story
    </div>
  </div>
)
```

### Product Feature Tour

```tsx
const features = [
  { name: "Camera", icon: "📷" },
  { name: "Battery", icon: "🔋" },
  { name: "Display", icon: "📱" },
  { name: "Performance", icon: "⚡" }
]

const { currentIndex, previousIndex } = useScrollProgress({
  totalBlocks: features.length,
  scrollsPerBlock: 3,
  onIndexChange: (current, previous) => {
    // Animate transition between features
    slideOut(features[previous])
    slideIn(features[current])
  }
})
```

### Block Options Explained

**`totalBlocks`**: Divides the 0-100 progress into N blocks
- `totalBlocks: 5` creates blocks at [0-20, 20-40, 40-60, 60-80, 80-100]
- Returns indices 0, 1, 2, 3, 4

**`scrollsPerBlock`**: Controls scroll sensitivity
- `scrollsPerBlock: 1` - Very sensitive, changes with every scroll (default)
- `scrollsPerBlock: 3` - Requires 3 scroll events to advance one block (recommended for arrays)
- `scrollsPerBlock: 5` - Very controlled, deliberate navigation

**`snapToBlocks`**: Snap behavior
- `false` - Smooth progress within blocks (default)
- `true` - Progress jumps directly from block to block, no in-between values

**`onIndexChange`**: Triggered when moving between blocks
```tsx
onIndexChange: (current, previous) => {
  console.log(`Changed from block ${previous} to ${current}`)
}
```

**`onBlockProgress`**: Triggered on every scroll with current block info
```tsx
onBlockProgress: (index, progress) => {
  console.log(`Block ${index} is ${progress}% complete`)
  // Use for animations within each block
}
```

## How it Works

1. **Activation**: Hover or touch the trigger element to activate progress tracking
2. **Direction Detection**: Learns your scroll preference automatically (when using 'system' mode)
3. **Progress Control**: Scroll in your natural direction to control progress
4. **Block Navigation**: When using `totalBlocks`, progress is divided into discrete sections
5. **Hold Behavior**: Progress either holds at current value or auto-decreases based on `isAutoHoldProgress`
6. **Completion**: When progress reaches 100%, the `onComplete` callback fires
7. **Reset**: Progress can be reset by scrolling in reverse or calling `resetProgress()`

## License

MIT � Dewan Meadown