Skip to content

feat: add carousel component with gesture support and animations#1

Merged
chakrihacker merged 4 commits into
mainfrom
feat/snap-corousel
Nov 29, 2025
Merged

feat: add carousel component with gesture support and animations#1
chakrihacker merged 4 commits into
mainfrom
feat/snap-corousel

Conversation

@chakrihacker

@chakrihacker chakrihacker commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

User description

  • Implemented performant swipe carousel with react-native-reanimated and gesture-handler
  • Added Jest setup for testing with mocks for react-native-svg and dimensions
  • Updated babel configuration to include react-native-reanimated plugin
  • Created jest configuration and setup files for testing
  • Enhanced README with installation instructions and usage examples
  • Added coverage reports for improved test visibility
  • Updated package.json with necessary dependencies and peer dependencies

Summary by Sourcery

Introduce a generic, animated swipe carousel component with gesture support, update its example and documentation, and set up a full Jest testing and coverage reporting pipeline.

New Features:

  • Add a reusable, generic Carousel component with gesture-based navigation, scaling, opacity animations, and optional gradient border support.
  • Provide an example app that fetches remote images and showcases the carousel with detail rendering.

Enhancements:

  • Document installation, peer dependencies, usage examples, and API reference for the carousel in the README.
  • Configure ESLint to understand Jest globals in test and Jest config files.
  • Enable the react-native-reanimated Babel plugin for proper animation support.

Build:

  • Update package.json devDependencies and peerDependencies to include gesture handler, reanimated, SVG, worklets, testing, and Jest ESLint tooling.
  • Adjust example app dependencies to match the carousel’s runtime requirements and React Native ecosystem.

Tests:

  • Replace the placeholder test with an extensive Carousel test suite covering rendering, props, gestures, edge cases, and hook behavior.
  • Add Jest configuration, setup (including mocks for SVG, Dimensions, and worklets), and coverage collection for the library.
  • Check in generated coverage reports for visibility into test coverage.

CodeAnt-AI Description

Ship interactive carousel component with docs, tests, and workflow

What Changed

  • Export a fully interactive carousel that animates centered slides, snaps on swipes, highlights the focused item with an optional gradient border, and triggers onItemChange only when the selection really changes.
  • Replace the example app with one that fetches live images, shows them inside the carousel wrapped by GestureHandlerRootView, and surfaces author/detail info along with error handling and a loading state.
  • Add Jest setup/mocks, ESLint hooks for Jest globals, a coverage workflow triggered on pull requests, and documentation plus dependency lists that spell out how to install and use the carousel.

Impact

✅ Gesture-driven carousel navigation
✅ Test coverage on every pull request
✅ Clearer setup guidance for carousel users

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

- Implemented performant swipe carousel with react-native-reanimated and gesture-handler
- Added Jest setup for testing with mocks for react-native-svg and dimensions
- Updated babel configuration to include react-native-reanimated plugin
- Created jest configuration and setup files for testing
- Enhanced README with installation instructions and usage examples
- Added coverage reports for improved test visibility
- Updated package.json with necessary dependencies and peer dependencies
@codeant-ai

codeant-ai Bot commented Nov 27, 2025

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@sourcery-ai

sourcery-ai Bot commented Nov 27, 2025

Copy link
Copy Markdown

Reviewer's Guide

Implements a new generic, animated swipe Carousel component using react-native-reanimated, gesture-handler, SVG gradient borders, and worklets; adds comprehensive tests, documentation, example usage, Jest config/mocks, ESLint Jest rules, coverage reporting, and updates dependencies/babel/peerDeps accordingly.

Sequence diagram for Carousel pan gesture and item change flow

sequenceDiagram
  actor User
  participant GestureHandlerRootView
  participant GestureDetector
  participant Carousel
  participant ReanimatedWorklet
  participant ScheduleOnRN
  participant ParentComponent

  User->>GestureHandlerRootView: Swipe horizontally
  GestureHandlerRootView->>GestureDetector: Deliver pan gesture events
  GestureDetector->>Carousel: panGesture.onUpdate(event)
  activate Carousel
  Carousel->>ReanimatedWorklet: Update translateX sharedValue
  ReanimatedWorklet-->>Carousel: New translateX clamped to bounds
  deactivate Carousel

  GestureDetector->>Carousel: panGesture.onEnd(event)
  activate Carousel
  Carousel->>ReanimatedWorklet: Compute targetIndex and snapToIndex(targetIndex)
  ReanimatedWorklet-->>Carousel: Spring animation for translateX and currentIndex
  Carousel->>ScheduleOnRN: scheduleOnRN(updateSelectedItem, targetIndex)
  deactivate Carousel

  ScheduleOnRN->>Carousel: updateSelectedItem(targetIndex)
  activate Carousel
  Carousel->>ParentComponent: onItemChange(newItem)
  deactivate Carousel

  ParentComponent-->>ParentComponent: setSelectedItem(newItem)
  ParentComponent->>Carousel: Rerender with updated selectedItem
Loading

Updated class diagram for Carousel component and related types

classDiagram
  direction LR

  class PanGestureEvent {
    +translationX: number
    +velocityX: number
  }

  class ImageSource {
    <<type alias>>
    string
    ImageSourcePropType
  }

  class CarouselItemBase {
    +image: ImageSource
  }

  class CarouselPropsT {
    +items: T[]
    +selectedItem: T
    +onItemChange(item: T) void
    +getItemId(item: T) string
    +renderItem(item: T, index: number, translateX: SharedValueNumber) ReactNode
    +renderDetail(item: T) ReactNode
    +itemSize: number
    +itemSpacing: number
    +minScale: number
    +maxScale: number
    +showGradientBorder: boolean
    +gradientColors: string[]
    +gradientId: string
  }

  class GradientBorder {
    +itemSize: number
    +gradientColors: string[]
    +gradientId: string
    +render() ReactNode
  }

  class CarouselItemT {
    +item: T
    +index: number
    +translateX: SharedValueNumber
    +itemSize: number
    +itemSpacing: number
    +minScale: number
    +maxScale: number
    +renderItem(item: T, index: number, translateX: SharedValueNumber) ReactNode
    -animatedStyle(): AnimatedStyle
    +render() ReactNode
  }

  class CarouselT {
    +items: T[]
    +selectedItem: T
    +onItemChange(item: T) void
    +getItemId(item: T) string
    +renderItem(item: T, index: number, translateX: SharedValueNumber) ReactNode
    +renderDetail(item: T) ReactNode
    +itemSize: number
    +itemSpacing: number
    +minScale: number
    +maxScale: number
    +showGradientBorder: boolean
    +gradientColors: string[]
    +gradientId: string
    -translateX: SharedValueNumber
    -currentIndex: SharedValueNumber
    -previousItemId: RefString
    -snapToIndex(index: number) void
    -updateSelectedItem(index: number) void
    -panGesture: PanGesture
    +render() ReactNode
  }

  class SharedValueNumber {
    <<Reanimated SharedValue>>
    +value: number
  }

  class PanGesture {
    <<gesture handler>>
    +withTestId(id: string) PanGesture
    +onUpdate(event: PanGestureEvent) PanGesture
    +onEnd(event: PanGestureEvent) PanGesture
  }

  class GestureDetectorComponent {
    +gesture: PanGesture
    +children: ReactNode
  }

  class ScheduleOnRN {
    <<function>>
    +scheduleOnRN(fn: Function, arg: any) void
  }

  CarouselT ..|> CarouselPropsT
  CarouselItemT ..|> CarouselItemBase

  CarouselT o-- CarouselItemT : renders
  CarouselT ..> GradientBorder : optional
  CarouselT ..> SharedValueNumber : uses
  CarouselT ..> PanGesture : configures
  CarouselT ..> GestureDetectorComponent : wraps content
  CarouselT ..> ScheduleOnRN : uses

  CarouselItemT ..> SharedValueNumber : animates
  GradientBorder ..> ImageSource : via SVG visuals
Loading

File-Level Changes

Change Details Files
Introduce a generic, animated swipe Carousel component with gesture support, scaling/opacity interpolation, and optional SVG gradient border and detail view.
  • Replace previous multiply helper export with a typed Carousel component using react-native-reanimated shared values and animated styles.
  • Use GestureHandler pan gesture to update a shared translateX value with velocity-based snapping via withSpring and a VELOCITY_THRESHOLD.
  • Maintain current index and selected item using shared values, a React ref, and a useEffect that syncs external selectedItem prop to internal translateX.
  • Implement CarouselItem subcomponent that positions items absolutely, computes distance from screen center, and interpolates scale/opacity based on itemSpacing and itemSize.
  • Implement GradientBorder subcomponent using react-native-svg LinearGradient and Circle with configurable colors, gradientId, and size derived from itemSize.
  • Expose typed helper types ImageSource and CarouselItemBase and a rich CarouselProps API including renderItem, renderDetail, sizing, spacing, scale, and gradient props.
src/index.tsx
Add comprehensive unit and interaction tests for the Carousel component including gestures, edge cases, and default behavior.
  • Replace placeholder test with an extensive test suite that covers rendering, custom props, gradient border behavior, renderDetail, edge cases for items/selectedItem, image source variants, CarouselItem behavior, pan gesture behavior including velocity and clamping, useEffect behavior on items/selectedItem changes, default values, and overall integration.
  • Use @testing-library/react-native for rendering and react-native-gesture-handler/jest-utils to simulate pan gestures and state transitions, asserting onItemChange callbacks and renderItem invocations.
  • Leverage Jest fake timers to advance animations triggered by withSpring and worklets in gesture end handlers.
src/__tests__/index.test.tsx
Update example app to showcase the new Carousel using live data and gradient border.
  • Replace multiply demo with a Picsum-powered image carousel that fetches images, maps them into items with an image field, and uses Carousel to display them with circular Image items and a detail view showing author and URL.
  • Wrap the app tree in GestureHandlerRootView, configure itemSize and gradientId, and add simple header styles for visual polish.
example/src/App.tsx
Document Carousel usage, API, and features in README with TypeScript examples.
  • Rewrite README to describe the Carousel features, installation steps including peer dependencies, and setup notes for gesture-handler and reanimated.
  • Add basic and advanced usage examples showing renderItem, renderDetail, generics, and gradient customization.
  • Add API reference table describing Carousel props and types, plus sections explaining gestures, animations, and gradient border behavior.
README.md
Set up Jest configuration, mocks, and ESLint integration for React Native, Reanimated, gesture-handler, and SVG.
  • Create jest.config.js with react-native preset, setupFiles for gesture-handler and custom jest-setup, transformIgnorePatterns for RN-related modules, and collectCoverageFrom to include src while excluding lib/example/tests.
  • Add jest-setup.js to initialize reanimated tests, mock react-native-svg components to View-based stubs (with testIDs), mock Dimensions.get to fixed values, and mock react-native-worklets scheduleOnRN to run synchronously.
  • Remove inline jest configuration from package.json in favor of standalone config.
  • Extend ESLint config to enable jest environment and recommended jest rules for test files and Jest config/ setup scripts.
jest.config.js
jest-setup.js
eslint.config.mjs
package.json
Adjust build and runtime configuration for Reanimated and dependencies, and add testing-related dependencies.
  • Add react-native-reanimated Babel plugin to babel.config.js (ensuring it is last in the plugin list).
  • Update root package.json devDependencies to include @testing-library/react-native, @types/react-test-renderer, eslint-plugin-jest, react-test-renderer, react-native-gesture-handler, react-native-reanimated, react-native-svg, and react-native-worklets.
  • Expand peerDependencies to require gesture-handler, reanimated, svg, and worklets alongside react and react-native.
  • Update example app package.json dependencies to include the same native libraries with compatible versions.
babel.config.js
package.json
example/package.json
Check in coverage artifacts demonstrating full test coverage of the Carousel implementation.
  • Add lcov HTML report files, clover.xml, lcov.info, coverage-final.json, and yarn.lock under the coverage directory, showing 100% coverage for src/index.tsx.
  • Ensure coverage includes source-decorated index.tsx HTML showing executed lines and branches.
coverage/index.html
coverage/index.tsx.html
coverage/base.css
coverage/block-navigation.js
coverage/clover.xml
coverage/coverage-final.json
coverage/lcov.info
coverage/lcov-report/index.html
coverage/lcov-report/index.tsx.html
coverage/lcov-report/base.css
coverage/lcov-report/block-navigation.js
coverage/lcov-report/prettify.css
coverage/lcov-report/prettify.js
coverage/lcov-report/sorter.js
coverage/lcov-report/favicon.png
coverage/lcov-report/sort-arrow-sprite.png
coverage/lcov-report/prettify.css
coverage/lcov-report/prettify.js
coverage/lcov-report/sorter.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Nov 27, 2025

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Carousel component with gesture-driven navigation, animated transitions, customizable gradient borders, and TypeScript support.
  • Documentation

    • Expanded README with features overview, API reference, demo examples, and usage guides.
  • Tests

    • Added comprehensive test suite and Jest configuration for React Native testing environment.
  • Chores

    • Updated build configurations and linting setup for improved development tooling.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds a fully implemented, generic Carousel component (gestures, reanimated animations, SVG gradient borders), extensive tests, example app wiring, Jest/ESLint/Babel test tooling, package dependency updates, expanded README, CI coverage workflow, and .gitignore entries.

Changes

Cohort / File(s) Summary
Documentation
README.md
Completely expanded README: Features, Installation, Demo, Peer Dependencies, Basic/Advanced usage, API Reference (detailed Carousel generic signature), Features Explained, and Notes.
Core Component
src/index.tsx
Replaced simple exported function with a typed, generic Carousel<T extends CarouselItemBase> component, ImageSource/CarouselItemBase types, GradientBorder (SVG), CarouselItem (animated), pan gesture handling, velocity-based snapping, and prop-driven synchronization.
Tests
src/__tests__/index.test.tsx
Replaced placeholder test with a comprehensive suite covering rendering, gradient border, renderDetail, edge cases, gesture/velocity behaviors, animation effects, and integration scenarios.
Example App
example/src/App.tsx
Reworked example to fetch Picsum images, manage items/selectedItem/loading, and render the new Carousel inside GestureHandlerRootView with image/detail rendering.
Tooling: Jest / Test Setup
jest.config.js, jest-setup.js
Added Jest config (RN preset, setupFiles, coverage/ignore rules, transformIgnorePatterns) and jest-setup mocks for reanimated, react-native-svg components, Dimensions, and react-native-worklets scheduling for deterministic tests.
Tooling: ESLint / Babel
eslint.config.mjs, babel.config.js
Added eslint-plugin-jest configs for test files and imports; added react-native-reanimated/plugin to Babel (as last plugin) in overrides.
Dependencies / Manifests
package.json, example/package.json
Added dev/peer dependencies for testing and RN libraries (react-native-gesture-handler, react-native-reanimated, react-native-svg, react-native-worklets, @testing-library/react-native, react-test-renderer, eslint-plugin-jest); removed inline jest block from root package.json; updated example deps.
CI / Git
.github/workflows/coverage.yml, .gitignore
Added GitHub Actions workflow for Jest coverage reporting on PRs; added nitrogen/ and coverage/ to .gitignore.

Sequence Diagram

sequenceDiagram
    actor User
    participant Gesture as "Pan Gesture Handler"
    participant Reanimated as "Reanimated (worklets)"
    participant Carousel as "Carousel Component"
    participant Callback as "onItemChange"

    User->>Gesture: drag (translateX)
    Gesture->>Reanimated: update shared translateX
    Reanimated->>Carousel: recalc animated styles (position/scale/opacity)
    Carousel->>Carousel: compute nearest/snap index from translateX

    alt Release with high velocity
        User->>Gesture: release (velocity)
        Gesture->>Reanimated: compute velocity-based target index
    else Release with low velocity
        User->>Gesture: release
        Gesture->>Reanimated: clamp & snap to nearest index
    end

    Reanimated->>Carousel: spring animate to snapped index
    Carousel->>Callback: invoke onItemChange if index changed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Review hotspots:
    • src/index.tsx — gesture math, reanimated worklets, velocity snap/clamping, SVG gradient integration
    • src/tests/index.test.tsx — test correctness and mocks vs runtime behavior
    • jest-setup.js & jest.config.js — mocks for react-native-svg/worklets and transformIgnorePatterns
    • babel.config.js — ensure reanimated plugin ordering
    • example/src/App.tsx — network fetching and state selection logic

Poem

🐰 I hopped through code with eager paws,
A carousel now spins with laws,
Gradients shimmer, items glide,
Tests keep every tumble tried,
Hop, spring, and circle — joy abounds! 🎠

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately summarizes the main change: introducing a carousel component with gesture support and animations, which is the primary focus of the PR.
Description check ✅ Passed The pull request description comprehensively covers all major changes including the carousel component implementation, testing infrastructure, documentation, and configuration updates.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5cec350 and 5a485b3.

📒 Files selected for processing (1)
  • README.md (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review

qodo-code-review Bot commented Nov 27, 2025

Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Gesture flood risk

Description: The 'panGesture' handler uses 'withTestId' and processes pointer input to update shared
state but does not implement any gesture throttling/debouncing or bounds checking beyond
simple clamping, potentially enabling malicious or accidental high-frequency gesture
events to trigger excessive JS-to-native scheduling via 'scheduleOnRN(updateSelectedItem,
...)', which could be abused to cause UI thread jank or minor denial-of-service in the
app.
index.tsx [286-316]

Referred Code
const panGesture = Gesture.Pan()
  .withTestId('carousel-pan-gesture')
  .onUpdate((event: PanGestureEvent) => {
    const newTranslateX =
      -currentIndex.value * itemSpacing + event.translationX;
    const minTranslateX = -(items.length - 1) * itemSpacing;
    const maxTranslateX = 0;

    translateX.value = Math.max(
      minTranslateX,
      Math.min(maxTranslateX, newTranslateX)
    );
  })
  .onEnd((event: PanGestureEvent) => {
    const currentTranslateX = translateX.value;
    const velocity = event.velocityX;

    let targetIndex = Math.round(-currentTranslateX / itemSpacing);

    if (Math.abs(velocity) > VELOCITY_THRESHOLD) {


 ... (clipped 10 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
No audit logs: The new carousel UI interactions (gesture-driven item changes) are not logged, but as a UI
component without sensitive actions this may be acceptable depending on app-wide logging
strategy.

Referred Code
const updateSelectedItem = (index: number) => {
  const clampedIndex = Math.max(0, Math.min(index, items.length - 1));
  const item = items[clampedIndex];
  if (item && getItemId(item) !== getItemId(selectedItem)) {
    onItemChange(item);
  }
};

const panGesture = Gesture.Pan()
  .withTestId('carousel-pan-gesture')
  .onUpdate((event: PanGestureEvent) => {
    const newTranslateX =
      -currentIndex.value * itemSpacing + event.translationX;
    const minTranslateX = -(items.length - 1) * itemSpacing;
    const maxTranslateX = 0;

    translateX.value = Math.max(
      minTranslateX,
      Math.min(maxTranslateX, newTranslateX)
    );
  })


 ... (clipped 17 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing guards: The gesture/end logic does not explicitly guard against empty items or invalid indices at
runtime, relying on clamping and tests; this may be fine for a pure UI component but
warrants verification.

Referred Code
  'worklet';
  const clampedIndex = Math.max(0, Math.min(index, items.length - 1));
  translateX.value = withSpring(-clampedIndex * itemSpacing, {
    damping: SPRING_DAMPING,
    stiffness: SPRING_STIFFNESS,
  });
  currentIndex.value = clampedIndex;
};

const updateSelectedItem = (index: number) => {
  const clampedIndex = Math.max(0, Math.min(index, items.length - 1));
  const item = items[clampedIndex];
  if (item && getItemId(item) !== getItemId(selectedItem)) {
    onItemChange(item);
  }
};

const panGesture = Gesture.Pan()
  .withTestId('carousel-pan-gesture')
  .onUpdate((event: PanGestureEvent) => {
    const newTranslateX =


 ... (clipped 26 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Input validation: The component trusts props like items and getItemId without runtime validation; for a UI
library this may be acceptable but should be confirmed by higher-level input validation.

Referred Code
export function Carousel<T extends CarouselItemBase>({
  items,
  selectedItem,
  onItemChange,
  getItemId,
  renderItem,
  renderDetail,
  itemSize = DEFAULT_ITEM_SIZE,
  itemSpacing = DEFAULT_ITEM_SPACING,
  minScale = DEFAULT_MIN_SCALE,
  maxScale = DEFAULT_MAX_SCALE,
  showGradientBorder = true,
  gradientColors = DEFAULT_GRADIENT_COLORS,
  gradientId = DEFAULT_GRADIENT_ID,
}: CarouselProps<T>) {
  const initialIndex = items.findIndex(
    (item) => getItemId(item) === getItemId(selectedItem)
  );
  const translateX = useSharedValue(-initialIndex * itemSpacing);

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@codeant-ai codeant-ai Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files label Nov 27, 2025
@qodo-code-review

qodo-code-review Bot commented Nov 27, 2025

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Exclude build artifacts from commits

Add the coverage/ directory to the .gitignore file. This will exclude generated
test coverage reports from the source repository, which is a standard best
practice.

Examples:

coverage/lcov-report/index.html [1-116]
<!doctype html>
<html lang="en">

<head>
    <title>Code coverage report for All files</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="prettify.css" />
    <link rel="stylesheet" href="base.css" />
    <link rel="shortcut icon" type="image/x-icon" href="favicon.png" />

 ... (clipped 106 lines)
coverage/coverage-final.json [1-2]
{"/Users/subramanya/Developer/github/react-native-swipe-carousel/src/index.tsx": {"path":"/Users/subramanya/Developer/github/react-native-swipe-carousel/src/index.tsx","statementMap":{"0":{"start":{"line":29,"column":21},"end":{"line":29,"column":51}},"1":{"start":{"line":32,"column":26},"end":{"line":32,"column":29}},"2":{"start":{"line":33,"column":29},"end":{"line":33,"column":32}},"3":{"start":{"line":34,"column":26},"end":{"line":34,"column":29}},"4":{"start":{"line":35,"column":26},"end":{"line":35,"column":29}},"5":{"start":{"line":36,"column":32},"end":{"line":36,"column":60}},"6":{"start":{"line":37,"column":28},"end":{"line":37,"column":47}},"7":{"start":{"line":40,"column":23},"end":{"line":40,"column":25}},"8":{"start":{"line":41,"column":25},"end":{"line":41,"column":27}},"9":{"start":{"line":42,"column":27},"end":{"line":42,"column":30}},"10":{"start":{"line":45,"column":20},"end":{"line":45,"column":21}},"11":{"start":{"line":46,"column":20},"end":{"line":46,"column":23}},"12":{"start":{"line":49,"column":32},"end":{"line":49,"column":33}},"13":{"start":{"line":50,"column":30},"end":{"line":50,"column":31}},"14":{"start":{"line":51,"column":31},"end":{"line":51,"column":32}},"15":{"start":{"line":54,"column":31},"end":{"line":54,"column":33}},"16":{"start":{"line":57,"column":30},"end":{"line":57,"column":33}},"17":{"start":{"line":58,"column":25},"end":{"line":58,"column":29}},"18":{"start":{"line":59,"column":25},"end":{"line":59,"column":29}},"19":{"start":{"line":60,"column":23},"end":{"line":60,"column":29}},"20":{"start":{"line":61,"column":23},"end":{"line":61,"column":29}},"21":{"start":{"line":102,"column":23},"end":{"line":102,"column":57}},"22":{"start":{"line":103,"column":25},"end":{"line":103,"column":41}},"23":{"start":{"line":104,"column":25},"end":{"line":104,"column":64}},"24":{"start":{"line":106,"column":2},"end":{"line":151,"column":4}},"25":{"start":{"line":125,"column":33},"end":{"line":125,"column":54}},"26":{"start":{"line":127,"column":16},"end":{"line":129,"column":58}},"27":{"start":{"line":130,"column":14},"end":{"line":137,"column":16}},"28":{"start":{"line":179,"column":19},"end":{"line":179,"column":35}},"29":{"start":{"line":180,"column":24},"end":{"line":180,"column":47}},"30":{"start":{"line":182,"column":24},"end":{"line":208,"column":4}},"31":{"start":{"line":183,"column":25},"end":{"line":183,"column":38}},"32":{"start":{"line":184,"column":19},"end":{"line":184,"column":38}},"33":{"start":{"line":185,"column":21},"end":{"line":185,"column":61}},"34":{"start":{"line":187,"column":31},"end":{"line":187,"column":75}},"35":{"start":{"line":188,"column":24},"end":{"line":188,"column":35}},"36":{"start":{"line":190,"column":18},"end":{"line":195,"column":5}},"37":{"start":{"line":197,"column":20},"end":{"line":202,"column":5}},"38":{"start":{"line":204,"column":4},"end":{"line":207,"column":6}},"39":{"start":{"line":210,"column":2},"end":{"line":227,"column":4}},"40":{"start":{"line":245,"column":23},"end":{"line":247,"column":3}},"41":{"start":{"line":246,"column":14},"end":{"line":246,"column":57}},"42":{"start":{"line":248,"column":21},"end":{"line":248,"column":64}},"43":{"start":{"line":249,"column":23},"end":{"line":249,"column":75}},"44":{"start":{"line":250,"column":25},"end":{"line":250,"column":56}},"45":{"start":{"line":252,"column":2},"end":{"line":267,"column":78}},"46":{"start":{"line":253,"column":26},"end":{"line":253,"column":49}},"47":{"start":{"line":254,"column":4},"end":{"line":266,"column":5}},"48":{"start":{"line":255,"column":6},"end":{"line":255,"column":45}},"49":{"start":{"line":256,"column":20},"end":{"line":258,"column":7}},"50":{"start":{"line":257,"column":18},"end":{"line":257,"column":51}},"51":{"start":{"line":259,"column":6},"end":{"line":265,"column":7}},"52":{"start":{"line":260,"column":8},"end":{"line":260,"column":35}},"53":{"start":{"line":261,"column":8},"end":{"line":264,"column":11}},"54":{"start":{"line":269,"column":22},"end":{"line":277,"column":3}},"55":{"start":{"line":271,"column":25},"end":{"line":271,"column":71}},"56":{"start":{"line":272,"column":4},"end":{"line":275,"column":7}},"57":{"start":{"line":276,"column":4},"end":{"line":276,"column":38}},"58":{"start":{"line":279,"column":29},"end":{"line":285,"column":3}},"59":{"start":{"line":280,"column":25},"end":{"line":280,"column":71}},"60":{"start":{"line":281,"column":17},"end":{"line":281,"column":36}},"61":{"start":{"line":282,"column":4},"end":{"line":284,"column":5}},"62":{"start":{"line":283,"column":6},"end":{"line":283,"column":25}},"63":{"start":{"line":287,"column":21},"end":{"line":315,"column":6}},"64":{"start":{"line":290,"column":8},"end":{"line":290,"column":62}},"65":{"start":{"line":291,"column":28},"end":{"line":291,"column":61}},"66":{"start":{"line":292,"column":28},"end":{"line":292,"column":29}},"67":{"start":{"line":294,"column":6},"end":{"line":297,"column":8}},"68":{"start":{"line":300,"column":32},"end":{"line":300,"column":48}},"69":{"start":{"line":301,"column":23},"end":{"line":301,"column":38}},"70":{"start":{"line":303,"column":24},"end":{"line":303,"column":68}},"71":{"start":{"line":305,"column":6},"end":{"line":311,"column":7}},"72":{"start":{"line":306,"column":8},"end":{"line":310,"column":9}},"73":{"start":{"line":307,"column":10},"end":{"line":307,"column":68}},"74":{"start":{"line":309,"column":10},"end":{"line":309,"column":53}},"75":{"start":{"line":313,"column":6},"end":{"line":313,"column":31}},"76":{"start":{"line":314,"column":6},"end":{"line":314,"column":52}},"77":{"start":{"line":317,"column":2},"end":{"line":377,"column":4}},"78":{"start":{"line":343,"column":14},"end":{"line":353,"column":16}}},"fnMap":{"0":{"name":"GradientBorder","decl":{"start":{"line":97,"column":9},"end":{"line":97,"column":23}},"loc":{"start":{"line":101,"column":24},"end":{"line":152,"column":1}},"line":101},"1":{"name":"(anonymous_1)","decl":{"start":{"line":124,"column":32},"end":{"line":124,"column":33}},"loc":{"start":{"line":124,"column":50},"end":{"line":138,"column":13}},"line":124},"2":{"name":"CarouselItem","decl":{"start":{"line":169,"column":9},"end":{"line":169,"column":21}},"loc":{"start":{"line":178,"column":25},"end":{"line":228,"column":1}},"line":178},"3":{"name":"(anonymous_3)","decl":{"start":{"line":182,"column":41},"end":{"line":182,"column":42}},"loc":{"start":{"line":182,"column":47},"end":{"line":208,"column":3}},"line":182},"4":{"name":"Carousel","decl":{"start":{"line":230,"column":16},"end":{"line":230,"column":24}},"loc":{"start":{"line":244,"column":21},"end":{"line":378,"column":1}},"line":244},"5":{"name":"(anonymous_5)","decl":{"start":{"line":246,"column":4},"end":{"line":246,"column":5}},"loc":{"start":{"line":246,"column":14},"end":{"line":246,"column":57}},"line":246},"6":{"name":"(anonymous_6)","decl":{"start":{"line":252,"column":12},"end":{"line":252,"column":13}},"loc":{"start":{"line":252,"column":18},"end":{"line":267,"column":3}},"line":252},"7":{"name":"(anonymous_7)","decl":{"start":{"line":257,"column":8},"end":{"line":257,"column":9}},"loc":{"start":{"line":257,"column":18},"end":{"line":257,"column":51}},"line":257},"8":{"name":"(anonymous_8)","decl":{"start":{"line":269,"column":22},"end":{"line":269,"column":23}},"loc":{"start":{"line":269,"column":41},"end":{"line":277,"column":3}},"line":269},"9":{"name":"(anonymous_9)","decl":{"start":{"line":279,"column":29},"end":{"line":279,"column":30}},"loc":{"start":{"line":279,"column":48},"end":{"line":285,"column":3}},"line":279},"10":{"name":"(anonymous_10)","decl":{"start":{"line":288,"column":14},"end":{"line":288,"column":15}},"loc":{"start":{"line":288,"column":42},"end":{"line":298,"column":5}},"line":288},"11":{"name":"(anonymous_11)","decl":{"start":{"line":299,"column":11},"end":{"line":299,"column":12}},"loc":{"start":{"line":299,"column":39},"end":{"line":315,"column":5}},"line":299},"12":{"name":"(anonymous_12)","decl":{"start":{"line":342,"column":23},"end":{"line":342,"column":24}},"loc":{"start":{"line":343,"column":14},"end":{"line":353,"column":16}},"line":343}},"branchMap":{"0":{"loc":{"start":{"line":127,"column":16},"end":{"line":129,"column":58}},"type":"cond-expr","locations":[{"start":{"line":128,"column":20},"end":{"line":128,"column":24}},{"start":{"line":129,"column":20},"end":{"line":129,"column":58}}],"line":127},"1":{"loc":{"start":{"line":237,"column":2},"end":{"line":237,"column":30}},"type":"default-arg","locations":[{"start":{"line":237,"column":13},"end":{"line":237,"column":30}}],"line":237},"2":{"loc":{"start":{"line":238,"column":2},"end":{"line":238,"column":36}},"type":"default-arg","locations":[{"start":{"line":238,"column":16},"end":{"line":238,"column":36}}],"line":238},"3":{"loc":{"start":{"line":239,"column":2},"end":{"line":239,"column":30}},"type":"default-arg","locations":[{"start":{"line":239,"column":13},"end":{"line":239,"column":30}}],"line":239},"4":{"loc":{"start":{"line":240,"column":2},"end":{"line":240,"column":30}},"type":"default-arg","locations":[{"start":{"line":240,"column":13},"end":{"line":240,"column":30}}],"line":240},"5":{"loc":{"start":{"line":241,"column":2},"end":{"line":241,"column":27}},"type":"default-arg","locations":[{"start":{"line":241,"column":23},"end":{"line":241,"column":27}}],"line":241},"6":{"loc":{"start":{"line":242,"column":2},"end":{"line":242,"column":42}},"type":"default-arg","locations":[{"start":{"line":242,"column":19},"end":{"line":242,"column":42}}],"line":242},"7":{"loc":{"start":{"line":243,"column":2},"end":{"line":243,"column":34}},"type":"default-arg","locations":[{"start":{"line":243,"column":15},"end":{"line":243,"column":34}}],"line":243},"8":{"loc":{"start":{"line":249,"column":38},"end":{"line":249,"column":74}},"type":"cond-expr","locations":[{"start":{"line":249,"column":58},"end":{"line":249,"column":70}},{"start":{"line":249,"column":73},"end":{"line":249,"column":74}}],"line":249},"9":{"loc":{"start":{"line":254,"column":4},"end":{"line":266,"column":5}},"type":"if","locations":[{"start":{"line":254,"column":4},"end":{"line":266,"column":5}},{"start":{},"end":{}}],"line":254},"10":{"loc":{"start":{"line":259,"column":6},"end":{"line":265,"column":7}},"type":"if","locations":[{"start":{"line":259,"column":6},"end":{"line":265,"column":7}},{"start":{},"end":{}}],"line":259},"11":{"loc":{"start":{"line":282,"column":4},"end":{"line":284,"column":5}},"type":"if","locations":[{"start":{"line":282,"column":4},"end":{"line":284,"column":5}},{"start":{},"end":{}}],"line":282},"12":{"loc":{"start":{"line":282,"column":8},"end":{"line":282,"column":59}},"type":"binary-expr","locations":[{"start":{"line":282,"column":8},"end":{"line":282,"column":12}},{"start":{"line":282,"column":16},"end":{"line":282,"column":59}}],"line":282},"13":{"loc":{"start":{"line":305,"column":6},"end":{"line":311,"column":7}},"type":"if","locations":[{"start":{"line":305,"column":6},"end":{"line":311,"column":7}},{"start":{},"end":{}}],"line":305},"14":{"loc":{"start":{"line":306,"column":8},"end":{"line":310,"column":9}},"type":"if","locations":[{"start":{"line":306,"column":8},"end":{"line":310,"column":9}},{"start":{"line":308,"column":15},"end":{"line":310,"column":9}}],"line":306},"15":{"loc":{"start":{"line":357,"column":9},"end":{"line":363,"column":9}},"type":"binary-expr","locations":[{"start":{"line":357,"column":9},"end":{"line":357,"column":27}},{"start":{"line":358,"column":10},"end":{"line":362,"column":12}}],"line":357},"16":{"loc":{"start":{"line":365,"column":7},"end":{"line":375,"column":7}},"type":"binary-expr","locations":[{"start":{"line":365,"column":7},"end":{"line":365,"column":19}},{"start":{"line":366,"column":8},"end":{"line":374,"column":15}}],"line":365}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":1,"20":1,"21":49,"22":49,"23":49,"24":49,"25":145,"26":145,"27":145,"28":140,"29":140,"30":140,"31":141,"32":141,"33":141,"34":141,"35":141,"36":141,"37":141,"38":141,"39":140,"40":50,"41":73,"42":50,"43":50,"44":50,"45":50,"46":50,"47":50,"48":3,"49":3,"50":9,"51":3,"52":2,"53":2,"54":50,"55":6,"56":6,"57":6,"58":50,"59":6,"60":6,"61":6,"62":3,"63":50,"64":4,"65":4,"66":4,"67":4,"68":6,"69":6,"70":6,"71":6,"72":3,"73":2,"74":1,"75":6,"76":6,"77":50,"78":140},"f":{"0":49,"1":145,"2":140,"3":141,"4":50,"5":73,"6":50,"7":9,"8":6,"9":6,"10":4,"11":6,"12":140},"b":{"0":[1,144],"1":[45],"2":[46],"3":[46],"4":[46],"5":[48],"6":[46],"7":[47],"8":[47,3],"9":[3,47],"10":[2,1],"11":[3,3],"12":[6,6],"13":[3,3],"14":[2,1],"15":[50,49],"16":[50,4]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"a407b76e782e7dc7aef424e051faf95844511d30"}
}

Solution Walkthrough:

Before:

# PR file structure includes generated artifacts
/src
  index.tsx
  ...
/coverage
  lcov-report/
    index.html
    base.css
    prettify.js
    ...
  clover.xml
  lcov.info
  ...
# .gitignore file (if present) is missing the coverage directory

After:

# .gitignore
...
# Build artifacts & logs
/node_modules
/lib
/coverage
...

# PR file structure no longer includes generated artifacts
/src
  index.tsx
  ...
# The 'coverage/' directory is now ignored by git.
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that the coverage/ directory, a build artifact, should not be committed, which is a crucial best practice for repository health and maintainability.

Medium
Possible issue
Fix potential infinite re-render loop
Suggestion Impact:The commit removed currentIndex and translateX from the useEffect dependency array and added a comment explaining they are SharedValues, aligning with the suggestion.

code diff:

-  }, [selectedItem, items, currentIndex, translateX, getItemId, itemSpacing]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps -- currentIndex and translateX are SharedValues (stable references)
+  }, [selectedItem, items, getItemId, itemSpacing]);

Remove currentIndex and translateX from the useEffect dependency array to
prevent potential bugs, as they are modified within the effect itself.

src/index.tsx [252-267]

 useEffect(() => {
   const currentItemId = getItemId(selectedItem);
   if (previousItemId.current !== currentItemId) {
     previousItemId.current = currentItemId;
     const index = items.findIndex(
       (item) => getItemId(item) === currentItemId
     );
     if (index !== -1) {
       currentIndex.value = index;
       translateX.value = withSpring(-index * itemSpacing, {
         damping: SPRING_DAMPING,
         stiffness: SPRING_STIFFNESS,
       });
     }
   }
-}, [selectedItem, items, currentIndex, translateX, getItemId, itemSpacing]);
+}, [selectedItem, items, getItemId, itemSpacing]);

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that currentIndex and translateX should not be in the useEffect dependency array as they are modified within the effect, which is a logic error that can cause unintended side effects.

Medium
Improve gesture snapping animation logic

Improve the gesture snapping logic by calculating a predicted final position
based on both translation and velocity to prevent the carousel from skipping
items on fast swipes.

src/index.tsx [300-316]

 .onEnd((event: PanGestureEvent) => {
   const currentTranslateX = translateX.value;
   const velocity = event.velocityX;
 
-  let targetIndex = Math.round(-currentTranslateX / itemSpacing);
-
-  if (Math.abs(velocity) > VELOCITY_THRESHOLD) {
-    if (velocity < 0) {
-      targetIndex = Math.min(targetIndex + 1, items.length - 1);
-    } else {
-      targetIndex = Math.max(targetIndex - 1, 0);
-    }
-  }
+  // Predict final position by adding velocity to current translation
+  const predictedTranslateX = currentTranslateX + velocity * 0.2; // Adjust multiplier as needed
+  const targetIndex = Math.round(-predictedTranslateX / itemSpacing);
 
   snapToIndex(targetIndex);
   scheduleOnRN(updateSelectedItem, targetIndex);
 });
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion identifies a real bug in the gesture snapping logic that could cause the carousel to skip an item, and proposes a more robust calculation that improves the user experience.

Medium
Prevent runtime error on first navigation
Suggestion Impact:The committed code removed the unconditional removal and now indirectly guards against undefined currentIndex by only calling toggleClass via makeCurrent after setting a valid index, preventing the first-call error.

code diff:

-    function toggleClass(index) {
-        missingCoverageElements
-            .item(currentIndex)
-            .classList.remove('highlighted');
-        missingCoverageElements.item(index).classList.add('highlighted');
-    }
-
-    function makeCurrent(index) {
-        toggleClass(index);
-        currentIndex = index;
-        missingCoverageElements.item(index).scrollIntoView({
-            behavior: 'smooth',
-            block: 'center',

Add a check in the toggleClass function to ensure currentIndex is a number
before attempting to access its properties, preventing a runtime error on the
first navigation.

coverage/lcov-report/block-navigation.js [24-29]

 // The NodeList of matching elements
 var missingCoverageElements = document.querySelectorAll(selector);
 
 var currentIndex;
 
 function toggleClass(index) {
-    missingCoverageElements
-        .item(currentIndex)
-        .classList.remove('highlighted');
+    if (typeof currentIndex === 'number') {
+        missingCoverageElements
+            .item(currentIndex)
+            .classList.remove('highlighted');
+    }
     missingCoverageElements.item(index).classList.add('highlighted');
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies a TypeError that will occur on the first navigation attempt because currentIndex is undefined. The proposed fix is accurate and prevents a crash, improving the script's robustness.

Medium
Prevent crash when no elements exist

Add a guard clause to the jump function to exit early if missingCoverageElements
is empty, preventing a crash when there are no uncovered blocks to navigate.

coverage/lcov-report/block-navigation.js [65-85]

 return function jump(event) {
+    if (missingCoverageElements.length === 0) {
+        return;
+    }
     if (
         document.getElementById('fileSearch') === document.activeElement &&
         document.activeElement != null
     ) {
         // if we're currently focused on the search input, we don't want to navigate
         return;
     }
 
     switch (event.which) {
         case 78: // n
         case 74: // j
             goToNext();
             break;
         case 66: // b
         case 75: // k
         case 80: // p
             goToPrevious();
             break;
     }
 };
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly points out a TypeError that occurs when navigation keys are pressed on a page with no uncovered code blocks. The fix prevents the script from crashing in this common scenario (e.g., a fully covered file).

Medium
Prevent crash if table body is missing

In onFilterInput, add a check to ensure the tbody element exists before
proceeding, preventing a potential TypeError if the table body is missing.

coverage/lcov-report/sorter.js [27-56]

 function onFilterInput() {
     const searchValue = document.getElementById('fileSearch').value;
-    const rows = document.getElementsByTagName('tbody')[0].children;
+    const tableBody = document.getElementsByTagName('tbody')[0];
+    if (!tableBody) {
+        return;
+    }
+    const rows = tableBody.children;
 
     // Try to create a RegExp from the searchValue. If it fails (invalid regex),
     // it will be treated as a plain text search
     let searchRegex;
     try {
         searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
     } catch (error) {
         searchRegex = null;
     }
 
     for (let i = 0; i < rows.length; i++) {
         const row = rows[i];
         let isMatch = false;
 
         if (searchRegex) {
             // If a valid regex was created, use it for matching
             isMatch = searchRegex.test(row.textContent);
         } else {
             // Otherwise, fall back to the original plain text search
             isMatch = row.textContent
                 .toLowerCase()
                 .includes(searchValue.toLowerCase());
         }
 
         row.style.display = isMatch ? '' : 'none';
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a potential TypeError if a tbody element is not found on the page. Adding a guard clause is a good defensive programming practice that makes the script more robust against unexpected HTML structures.

Low
  • Update

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

Blocking issues:

  • User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities (link)
  • User controlled data in a colNode.innerHTML is an anti-pattern that can lead to XSS vulnerabilities (link)

General comments:

  • In jest-setup.js, the mocked Dimensions.get returns an object with a window field, but the component calls Dimensions.get('window').width, so the mock should return the window object itself (e.g. { width, height }) rather than { window: { width, height } } to match the real API shape.
  • The coverage HTML/JSON/LCOV artifacts are committed into the repo; consider removing these from source control and adding the coverage directory to .gitignore so they are generated only locally/CI.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `jest-setup.js`, the mocked `Dimensions.get` returns an object with a `window` field, but the component calls `Dimensions.get('window').width`, so the mock should return the window object itself (e.g. `{ width, height }`) rather than `{ window: { width, height } }` to match the real API shape.
- The `coverage` HTML/JSON/LCOV artifacts are committed into the repo; consider removing these from source control and adding the coverage directory to `.gitignore` so they are generated only locally/CI.

## Individual Comments

### Comment 1
<location> `src/index.tsx:248-249` </location>
<code_context>
+  const initialIndex = items.findIndex(
+    (item) =&gt; getItemId(item) === getItemId(selectedItem)
+  );
+  const translateX = useSharedValue(-initialIndex * itemSpacing);
+  const currentIndex = useSharedValue(initialIndex &gt;= 0 ? initialIndex : 0);
+  const previousItemId = useRef(getItemId(selectedItem));
</code_context>

<issue_to_address>
**issue:** Guard `translateX` initialization when the selected item is not found to avoid misaligned initial position.

If `selectedItem` isn’t in `items`, `initialIndex` is `-1`. While `currentIndex` clamps this to `0`, `translateX` becomes `+itemSpacing`, briefly offsetting the first item until the next update. Consider clamping `initialIndex` (e.g. `const safeInitialIndex = Math.max(0, initialIndex)`) and using that for both `translateX` and `currentIndex` so the carousel starts centered on index 0 in this case.
</issue_to_address>

### Comment 2
<location> `src/index.tsx:130-137` </location>
<code_context>
+            x2={GRADIENT_END_X}
+            y2={GRADIENT_END_Y}
+          >
+            {gradientColors.map((color, index) => {
+              const colorCount = gradientColors.length;
+              const offset =
+                colorCount === 1
+                  ? '0%'
+                  : `${(index / (colorCount - 1)) * 100}%`;
+              return (
+                <Stop
+                  key={color}
+                  offset={offset}
+                  stopColor={color}
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Using `color` as the React key can lead to unstable keys when gradient colors repeat.

Here `gradientColors.map` uses `key={color}`. If the array contains duplicate colors or the order changes while duplicates remain, keys become non-unique/unstable and can confuse React’s reconciliation. Consider using `key={index}` or a composite like `${color}-${index}` to ensure stable, unique keys.

```suggestion
              return (
                <Stop
                  key={`${color}-${index}`}
                  offset={offset}
                  stopColor={color}
                  stopOpacity={GRADIENT_STOP_OPACITY}
                />
              );
```
</issue_to_address>

### Comment 3
<location> `jest-setup.js:40-47` </location>
<code_context>
+
+// Mock Dimensions
+const { Dimensions } = require('react-native');
+const mockDimensions = {
+  window: {
+    width: 375,
+    height: 812,
+  },
+};
+
+jest.spyOn(Dimensions, 'get').mockReturnValue(mockDimensions);
+
+// Mock react-native-worklets scheduleOnRN to execute synchronously in tests
</code_context>

<issue_to_address>
**issue (testing):** The Dimensions mock shape does not match `Dimensions.get('window')`, which can break width/height usage in tests.

In this setup `Dimensions.get('window').width` in app code will resolve to `mockDimensions.window.width`, but the mock returns `{ window: { width, height } }` directly. That makes `Dimensions.get('window')` return `{ window: { ... } }`, so `.width` is `undefined` in tests. To match React Native’s API, the mock should return a flat `{ width, height }` object for the requested dimension type, e.g.:

```js
const mockDimensions = { width: 375, height: 812 };
jest.spyOn(Dimensions, 'get').mockImplementation(type =>
  type === 'window' ? mockDimensions : /* other types if needed */ mockDimensions,
);
```
</issue_to_address>

### Comment 4
<location> `src/__tests__/index.test.tsx:542-551` </location>
<code_context>
+      jest.useRealTimers();
+    });
+
+    it('should handle pan gesture with positive translationX', () => {
+      const items = createTestItems(3);
+      const selectedItem = items[0]!;
+
+      render(
+        <Carousel
+          items={items}
+          selectedItem={selectedItem}
+          onItemChange={mockOnItemChange}
+          getItemId={mockGetItemId}
+          renderItem={mockRenderItem}
+        />
+      );
+
+      const gesture = getByGestureTestId('carousel-pan-gesture');
+
+      fireGestureHandler(gesture, [
+        { state: State.BEGAN, translationX: 0 },
+        { state: State.ACTIVE, translationX: 100 },
+        { state: State.END, translationX: 100 },
+      ]);
+
+      jest.advanceTimersByTime(1000);
+    });
+
</code_context>

<issue_to_address>
**suggestion (testing):** Pan gesture tests don’t assert any observable behavior, so they won’t catch regressions in gesture handling.

These tests currently only trigger the gesture and advance timers, but never assert outcomes, so they’ll still pass if the gesture logic breaks. Please add behavioral expectations, e.g.:
- `expect(mockOnItemChange).not.toHaveBeenCalled()` when the index should remain unchanged.
- `expect(mockOnItemChange).toHaveBeenCalledWith(expectedItem)` when a swipe should change the item.
- Optionally, verify that `renderDetail` still receives the same `selectedItem` after a no-op gesture.
This will ensure the tests actually validate gesture behavior rather than just execution.

Suggested implementation:

```typescript
    it('should handle pan gesture with positive translationX', () => {
      const items = createTestItems(3);
      const selectedItem = items[0]!;

      render(
        <Carousel
          items={items}
          selectedItem={selectedItem}
          onItemChange={mockOnItemChange}
          getItemId={mockGetItemId}
          renderItem={mockRenderItem}
        />
      );

      const gesture = getByGestureTestId('carousel-pan-gesture');

      fireGestureHandler(gesture, [
        { state: State.BEGAN, translationX: 0 },
        { state: State.ACTIVE, translationX: 100 },
        { state: State.END, translationX: 100 },
      ]);

      jest.advanceTimersByTime(1000);

      // Swiping right from the first item should not change the selected index
      expect(mockOnItemChange).not.toHaveBeenCalled();

      // Ensure the rendered item is still the original selected item
      expect(mockRenderItem).toHaveBeenCalledWith(
        expect.objectContaining({ item: selectedItem })
      );
    });

```

If `mockRenderItem` has a different call signature (e.g. `(args: { item: TestItem; index: number; isActive: boolean })`), the `expect.objectContaining({ item: selectedItem })` matcher will still be compatible. If instead it is called with positional arguments like `(item, index, isActive)`, you should update the assertion to:

```ts
expect(mockRenderItem).toHaveBeenCalledWith(
  selectedItem,
  expect.any(Number),
  expect.any(Boolean)
);
```

Also ensure that `mockOnItemChange` and `mockRenderItem` are reset between tests (e.g. via `jest.clearAllMocks()` in a global `beforeEach`) so these expectations are not polluted by calls from other tests.
</issue_to_address>

### Comment 5
<location> `src/__tests__/index.test.tsx:784-793` </location>
<code_context>
+      expect(mockOnItemChange).toHaveBeenCalledWith(items[1]);
+    });
+
+    it('should not call onItemChange when item does not change via gesture', () => {
+      const items = createTestItems(3);
+      const selectedItem = items[0]!;
+
+      render(
+        <Carousel
+          items={items}
+          selectedItem={selectedItem}
+          onItemChange={mockOnItemChange}
+          getItemId={mockGetItemId}
+          renderItem={mockRenderItem}
+        />
+      );
+
+      mockOnItemChange.mockClear();
+
+      const gesture = getByGestureTestId('carousel-pan-gesture');
+
+      fireGestureHandler(gesture, [
+        { state: State.BEGAN, translationX: 0 },
+        { state: State.ACTIVE, translationX: 0 },
+        { state: State.END, translationX: 0, velocityX: 0 },
+      ]);
+
+      jest.advanceTimersByTime(1000);
+
+      // Should stay on same item, so onItemChange may not be called
+      // or called with same item
+    });
</code_context>

<issue_to_address>
**issue (testing):** Test for gesture not changing item lacks an assertion and currently doesn’t verify the behavior it describes.

This test name implies `onItemChange` is not called when the item doesn’t change, but the test has no expectations. Please add an explicit assertion (e.g. `expect(mockOnItemChange).not.toHaveBeenCalled();`) so the behavior is actually verified. If calling `onItemChange` with the same item is allowed, assert that it’s only called with `selectedItem` and document that behavior in the test.
</issue_to_address>

### Comment 6
<location> `coverage/lcov-report/sorter.js:84-85` </location>
<code_context>
                colNode.innerHTML =
                    colNode.innerHTML + '<span class="sorter"></span>';
</code_context>

<issue_to_address>
**security (javascript.browser.security.insecure-document-method):** User controlled data in methods like `innerHTML`, `outerHTML` or `document.write` is an anti-pattern that can lead to XSS vulnerabilities

*Source: opengrep*
</issue_to_address>

### Comment 7
<location> `coverage/lcov-report/sorter.js:84-85` </location>
<code_context>
                colNode.innerHTML =
                    colNode.innerHTML + '<span class="sorter"></span>';
</code_context>

<issue_to_address>
**security (javascript.browser.security.insecure-innerhtml):** User controlled data in a `colNode.innerHTML` is an anti-pattern that can lead to XSS vulnerabilities

*Source: opengrep*
</issue_to_address>

### Comment 8
<location> `coverage/lcov-report/block-navigation.js:2-86` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 9
<location> `coverage/lcov-report/block-navigation.js:4` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 10
<location> `coverage/lcov-report/block-navigation.js:7` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 11
<location> `coverage/lcov-report/block-navigation.js:10` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 12
<location> `coverage/lcov-report/block-navigation.js:13-17` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 13
<location> `coverage/lcov-report/block-navigation.js:20` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 14
<location> `coverage/lcov-report/block-navigation.js:22` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 15
<location> `coverage/lcov-report/block-navigation.js:24-29` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 16
<location> `coverage/lcov-report/block-navigation.js:31-39` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 17
<location> `coverage/lcov-report/block-navigation.js:41-50` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 18
<location> `coverage/lcov-report/block-navigation.js:42` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 19
<location> `coverage/lcov-report/block-navigation.js:52-63` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 20
<location> `coverage/lcov-report/block-navigation.js:53` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 21
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 22
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 23
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 24
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 25
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 26
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 27
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 28
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 29
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

### Comment 30
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))

<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.

From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/index.tsx
Comment on lines +248 to +249
const translateX = useSharedValue(-initialIndex * itemSpacing);
const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Guard translateX initialization when the selected item is not found to avoid misaligned initial position.

If selectedItem isn’t in items, initialIndex is -1. While currentIndex clamps this to 0, translateX becomes +itemSpacing, briefly offsetting the first item until the next update. Consider clamping initialIndex (e.g. const safeInitialIndex = Math.max(0, initialIndex)) and using that for both translateX and currentIndex so the carousel starts centered on index 0 in this case.

Comment thread src/index.tsx
Comment thread jest-setup.js
Comment on lines +40 to +47
const mockDimensions = {
window: {
width: 375,
height: 812,
},
};

jest.spyOn(Dimensions, 'get').mockReturnValue(mockDimensions);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): The Dimensions mock shape does not match Dimensions.get('window'), which can break width/height usage in tests.

In this setup Dimensions.get('window').width in app code will resolve to mockDimensions.window.width, but the mock returns { window: { width, height } } directly. That makes Dimensions.get('window') return { window: { ... } }, so .width is undefined in tests. To match React Native’s API, the mock should return a flat { width, height } object for the requested dimension type, e.g.:

const mockDimensions = { width: 375, height: 812 };
jest.spyOn(Dimensions, 'get').mockImplementation(type =>
  type === 'window' ? mockDimensions : /* other types if needed */ mockDimensions,
);

Comment on lines +542 to +551
it('should handle pan gesture with positive translationX', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;

render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Pan gesture tests don’t assert any observable behavior, so they won’t catch regressions in gesture handling.

These tests currently only trigger the gesture and advance timers, but never assert outcomes, so they’ll still pass if the gesture logic breaks. Please add behavioral expectations, e.g.:

  • expect(mockOnItemChange).not.toHaveBeenCalled() when the index should remain unchanged.
  • expect(mockOnItemChange).toHaveBeenCalledWith(expectedItem) when a swipe should change the item.
  • Optionally, verify that renderDetail still receives the same selectedItem after a no-op gesture.
    This will ensure the tests actually validate gesture behavior rather than just execution.

Suggested implementation:

    it('should handle pan gesture with positive translationX', () => {
      const items = createTestItems(3);
      const selectedItem = items[0]!;

      render(
        <Carousel
          items={items}
          selectedItem={selectedItem}
          onItemChange={mockOnItemChange}
          getItemId={mockGetItemId}
          renderItem={mockRenderItem}
        />
      );

      const gesture = getByGestureTestId('carousel-pan-gesture');

      fireGestureHandler(gesture, [
        { state: State.BEGAN, translationX: 0 },
        { state: State.ACTIVE, translationX: 100 },
        { state: State.END, translationX: 100 },
      ]);

      jest.advanceTimersByTime(1000);

      // Swiping right from the first item should not change the selected index
      expect(mockOnItemChange).not.toHaveBeenCalled();

      // Ensure the rendered item is still the original selected item
      expect(mockRenderItem).toHaveBeenCalledWith(
        expect.objectContaining({ item: selectedItem })
      );
    });

If mockRenderItem has a different call signature (e.g. (args: { item: TestItem; index: number; isActive: boolean })), the expect.objectContaining({ item: selectedItem }) matcher will still be compatible. If instead it is called with positional arguments like (item, index, isActive), you should update the assertion to:

expect(mockRenderItem).toHaveBeenCalledWith(
  selectedItem,
  expect.any(Number),
  expect.any(Boolean)
);

Also ensure that mockOnItemChange and mockRenderItem are reset between tests (e.g. via jest.clearAllMocks() in a global beforeEach) so these expectations are not polluted by calls from other tests.

Comment on lines +784 to +793
it('should not call onItemChange when item does not change via gesture', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;

render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): Test for gesture not changing item lacks an assertion and currently doesn’t verify the behavior it describes.

This test name implies onItemChange is not called when the item doesn’t change, but the test has no expectations. Please add an explicit assertion (e.g. expect(mockOnItemChange).not.toHaveBeenCalled();) so the behavior is actually verified. If calling onItemChange with the same item is allowed, assert that it’s only called with selectedItem and document that behavior in the test.

Comment thread coverage/lcov-report/prettify.js Outdated
Comment thread coverage/lcov-report/prettify.js Outdated
Comment thread coverage/lcov-report/prettify.js Outdated
Comment thread coverage/lcov-report/prettify.js Outdated
Comment thread coverage/lcov-report/prettify.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
coverage/clover.xml (1)

1-88: Coverage artifacts should be excluded from version control.

This is another coverage report artifact that should typically be in .gitignore (see similar comment on coverage/lcov.info).

example/package.json (1)

18-22: Version alignment concern already flagged.

The version mismatches between these dependencies and the main package.json have been flagged in the review of package.json. Consider aligning versions for consistency.

coverage/lcov-report/index.html (1)

2-115: Static coverage HTML duplicates earlier coverage-artifact concern

This is another Istanbul-generated file under coverage/. The earlier suggestion to ignore coverage/ in version control (and tooling) applies here as well to reduce churn.

🧹 Nitpick comments (13)
coverage/lcov.info (1)

1-141: Consider excluding coverage artifacts from version control.

Coverage reports are typically generated during CI runs and don't need to be committed to the repository. They can bloat the repository history and become stale.

Consider adding these entries to .gitignore:

+# Coverage reports
+coverage/

Alternatively, if you want to keep coverage reports for visibility, consider using a separate coverage reporting service or generating them only in CI.

coverage/lcov-report/sorter.js (1)

1-210: Generated coverage UI files should be excluded from version control.

This is a generated file from the Istanbul/NYC coverage tool that provides interactive sorting/filtering in the coverage HTML report. These files should typically be in .gitignore along with other coverage artifacts.

This is covered by the same recommendation to add coverage/ to .gitignore (see comment on coverage/lcov.info).

coverage/lcov-report/prettify.js (1)

1-2: Generated coverage UI files should be excluded from version control.

This is a minified syntax highlighting library (Google Code Prettify) used by the coverage HTML reports. Like other coverage artifacts, it should be in .gitignore.

This is covered by the same recommendation to add coverage/ to .gitignore (see comment on coverage/lcov.info). The static analysis warnings are expected in minified generated code and can be safely ignored.

package.json (1)

85-88: Version differences between main and example packages exist but are minor and pose no security risk.

The packages in lines 85-88 do have newer versions in the main package.json compared to example/package.json:

  • react-native-gesture-handler: 2.29.1 (main) vs 2.28.0 (example)
  • react-native-reanimated: 4.1.5 (main) vs 4.1.1 (example)
  • react-native-svg: 15.15.0 (main) vs 15.12.1 (example)
  • react-native-worklets: 0.6.1 (main) vs 0.5.1 (example)

All versions are free of security vulnerabilities. Since these are devDependencies in the main package (used for library development) and the example app pulls the library from the monorepo rather than npm, consider whether alignment is necessary or merely a nice-to-have consistency improvement.

coverage/coverage-final.json (1)

1-2: Avoid committing generated coverage JSON

This looks like Jest/Istanbul output; committing coverage/coverage-final.json will create noisy diffs on every test run. Consider adding coverage/ to .gitignore and letting CI generate coverage reports instead of versioning them.

coverage/lcov-report/base.css (1)

1-224: Vendor-style coverage CSS – consider excluding from linters

This file matches the standard Istanbul coverage stylesheet, including intentional “duplicate” properties (e.g., color fallbacks and height hacks). Rather than modifying vendor CSS to satisfy Biome/ESLint, I’d suggest excluding coverage/** from linting/formatting so these generated assets don’t generate noise.

coverage/lcov-report/block-navigation.js (1)

1-87: Avoid global /* eslint-disable */ on generated coverage JS

Since this is Istanbul-generated helper code, the blanket /* eslint-disable */ is understandable but can trip lint rules about unlimited disables. A cleaner approach is to ignore coverage/** in your ESLint/Biome config (or .eslintignore) so generated scripts like this aren’t linted at all, instead of carrying a global disable comment.

README.md (1)

19-30: Consider documenting supported React Native versions

The install/peer dependency section is clear, but since libraries like react-native-gesture-handler only support relatively recent React Native versions (they officially track the last few RN minors), it may help consumers if you explicitly state which RN versions this carousel supports. (docs.swmansion.com)

Adding a short “Compatibility” note (e.g., “Tested with RN ≥ X.Y”) would set expectations for users integrating the library.

src/index.tsx (2)

29-29: SCREEN_WIDTH won't update on orientation or window resize.

This value is computed once at module load time. If the device rotates or the window resizes (e.g., split-screen on tablets), the carousel will use stale dimensions.

Consider using useWindowDimensions hook inside the component:

+import { Dimensions, View, type ImageSourcePropType, useWindowDimensions } from 'react-native';
-import { Dimensions } from 'react-native';
-import { View, type ImageSourcePropType } from 'react-native';

Then inside the Carousel component:

const { width: screenWidth } = useWindowDimensions();

And pass screenWidth as a prop to CarouselItem.


130-137: Using color as key may cause issues with duplicate gradient colors.

If gradientColors contains duplicates (e.g., ['red', 'blue', 'red']), React will warn about duplicate keys and may incorrectly recycle elements.

Use the index as part of the key:

-                <Stop
-                  key={color}
+                <Stop
+                  key={`${color}-${index}`}
                   offset={offset}
                   stopColor={color}
                   stopOpacity={GRADIENT_STOP_OPACITY}
                 />
example/src/App.tsx (1)

24-48: Consider adding AbortController for cleanup.

If the component unmounts before fetch completes, setItems and setSelectedItem will be called on an unmounted component. For an example app this is minor, but worth noting.

  useEffect(() => {
+    const controller = new AbortController();
    const fetchImages = async () => {
      try {
-        const response = await fetch('https://picsum.photos/v2/list?limit=5');
+        const response = await fetch('https://picsum.photos/v2/list?limit=5', {
+          signal: controller.signal,
+        });
        // ... rest of implementation
      } catch (error) {
+        if (error instanceof Error && error.name === 'AbortError') return;
        console.error('Error fetching images:', error);
      }
      // ...
    };

    fetchImages();
+    return () => controller.abort();
  }, []);
src/__tests__/index.test.tsx (2)

542-565: Test has no assertions - consider adding expectations.

This test fires a pan gesture but doesn't verify any resulting state or behavior. Without assertions, it only confirms the gesture doesn't throw an error.

Add assertions to verify expected behavior:

      fireGestureHandler(gesture, [
        { state: State.BEGAN, translationX: 0 },
        { state: State.ACTIVE, translationX: 100 },
        { state: State.END, translationX: 100 },
      ]);

      jest.advanceTimersByTime(1000);
+
+      // Positive translation should not change item when at first index
+      expect(mockOnItemChange).not.toHaveBeenCalled();
    });

567-640: Multiple gesture tests lack assertions.

Tests at lines 567, 592, and 617 fire gestures without verifying outcomes. These function as smoke tests only.

Add meaningful assertions. For example, for clamping tests:

it('should handle pan gesture clamping to minTranslateX', () => {
  const items = createTestItems(3);
  const selectedItem = items[0]!;

  render(/* ... */);

  const gesture = getByGestureTestId('carousel-pan-gesture');

  fireGestureHandler(gesture, [
    { state: State.BEGAN, translationX: 0 },
    { state: State.ACTIVE, translationX: -1000 },
    { state: State.END, translationX: -1000, velocityX: 0 },
  ]);

  jest.advanceTimersByTime(1000);

  // Should clamp to last item
  expect(mockOnItemChange).toHaveBeenCalledWith(items[2]);
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9b8b24 and 62a75d8.

⛔ Files ignored due to path filters (3)
  • coverage/lcov-report/favicon.png is excluded by !**/*.png
  • coverage/lcov-report/sort-arrow-sprite.png is excluded by !**/*.png
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (20)
  • README.md (1 hunks)
  • babel.config.js (1 hunks)
  • coverage/clover.xml (1 hunks)
  • coverage/coverage-final.json (1 hunks)
  • coverage/lcov-report/base.css (1 hunks)
  • coverage/lcov-report/block-navigation.js (1 hunks)
  • coverage/lcov-report/index.html (1 hunks)
  • coverage/lcov-report/index.tsx.html (1 hunks)
  • coverage/lcov-report/prettify.css (1 hunks)
  • coverage/lcov-report/prettify.js (1 hunks)
  • coverage/lcov-report/sorter.js (1 hunks)
  • coverage/lcov.info (1 hunks)
  • eslint.config.mjs (2 hunks)
  • example/package.json (1 hunks)
  • example/src/App.tsx (2 hunks)
  • jest-setup.js (1 hunks)
  • jest.config.js (1 hunks)
  • package.json (1 hunks)
  • src/__tests__/index.test.tsx (1 hunks)
  • src/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/__tests__/index.test.tsx (1)
src/index.tsx (2)
  • CarouselItemBase (67-69)
  • Carousel (230-379)
example/src/App.tsx (1)
src/index.tsx (1)
  • Carousel (230-379)
src/index.tsx (1)
jest-setup.js (5)
  • React (6-6)
  • Svg (21-21)
  • Defs (23-23)
  • Stop (25-25)
  • Circle (22-22)
🪛 Biome (2.1.2)
coverage/lcov-report/base.css

[error] 6-6: Duplicate font names are redundant and unnecessary: Helvetica

Remove duplicate font names within the property

(lint/suspicious/noDuplicateFontNames)


[error] 54-54: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

color is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)


[error] 219-219: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

height is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)

coverage/lcov-report/prettify.js

[error] 2-2: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 2-2: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 2-2: Shouldn't redeclare 'ar'. Consider to delete it or rename it.

'ar' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'ar'. Consider to delete it or rename it.

'ar' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'at'. Consider to delete it or rename it.

'at' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'ak'. Consider to delete it or rename it.

'ak' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'ak'. Consider to delete it or rename it.

'ak' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'am'. Consider to delete it or rename it.

'am' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'ag'. Consider to delete it or rename it.

'ag' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'af'. Consider to delete it or rename it.

'af' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'ak'. Consider to delete it or rename it.

'ak' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'am'. Consider to delete it or rename it.

'am' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'ak'. Consider to delete it or rename it.

'ak' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'ag'. Consider to delete it or rename it.

'ag' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'V'. Consider to delete it or rename it.

'V' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'U'. Consider to delete it or rename it.

'U' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'ae'. Consider to delete it or rename it.

'ae' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'Y'. Consider to delete it or rename it.

'Y' is defined here:

(lint/suspicious/noRedeclare)


[error] 2-2: Shouldn't redeclare 'W'. Consider to delete it or rename it.

'W' is defined here:

(lint/suspicious/noRedeclare)

🪛 GitHub Check: lint
coverage/lcov-report/block-navigation.js

[warning] 1-1:
Unexpected unlimited 'eslint-disable' comment. Specify some rule names to disable

src/__tests__/index.test.tsx

[warning] 617-617:
Test has no assertions


[warning] 592-592:
Test has no assertions


[warning] 567-567:
Test has no assertions


[warning] 542-542:
Test has no assertions

example/src/App.tsx

[warning] 83-83:
Inline style: { justifyContent: 'center', alignItems: 'center' }


[warning] 60-60:
Inline style: { flex: 1 }


[warning] 52-52:
Inline style: { flex: 1 }

coverage/lcov-report/sorter.js

[warning] 1-1:
Unexpected unlimited 'eslint-disable' comment. Specify some rule names to disable

coverage/lcov-report/prettify.js

[warning] 1-1:
Unexpected unlimited 'eslint-disable' comment. Specify some rule names to disable

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (7)
babel.config.js (1)

6-8: LGTM!

The react-native-reanimated plugin is correctly configured and positioned last in the plugins array, as required by the library documentation.

eslint.config.mjs (1)

4-45: LGTM!

The Jest plugin integration is correctly configured:

  • Properly scoped to test files (**/__tests__/**/*, **/*.test.{ts,tsx,js,jsx})
  • Separate configuration for Jest setup files
  • Applies recommended Jest rules and globals
coverage/lcov-report/prettify.css (1)

1-1: Generated prettify stylesheet – no action needed

This looks like stock prettify/Istanbul CSS. I’d either keep it as-is or, if you follow the earlier advice, stop versioning coverage/ entirely so these generated assets don’t need maintenance.

jest.config.js (1)

1-25: Jest config is consistent with the RN + gesture/reanimated stack

Preset, setupFiles, coverage globs, and transformIgnorePatterns are coherent for this React Native + gesture-handler + Reanimated + SVG/worklets setup. I don’t see configuration issues here; just ensure you run the full test suite (including coverage) on CI to validate it in your environment.

src/index.tsx (1)

230-379: Well-structured carousel implementation.

The component correctly:

  • Handles controlled selectedItem changes via useEffect
  • Implements smooth spring animations for snapping
  • Clamps gesture translations to valid bounds
  • Uses scheduleOnRN to bridge worklet→JS for state updates
example/src/App.tsx (1)

65-94: Good example demonstrating Carousel usage.

The example properly shows:

  • Dynamic data fetching with loading state
  • Custom renderItem with image styling
  • Custom renderDetail for selected item info
  • Proper getItemId and onItemChange handling
src/__tests__/index.test.tsx (1)

1-33: Comprehensive test suite with good coverage.

The test suite thoroughly covers:

  • Basic rendering scenarios
  • Custom props handling
  • Edge cases (empty items, single item, non-existent selectedItem)
  • Gesture interactions
  • Component integration

The test helpers (createTestItems, mock functions) are well-structured for reuse.

Comment thread coverage/lcov-report/index.tsx.html Outdated
Comment on lines +1 to +1219

<!doctype html>
<html lang="en">

<head>
<title>Code coverage report for index.tsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>

<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="index.html">All files</a> index.tsx</h1>
<div class='clearfix'>

<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>79/79</span>
</div>


<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>27/27</span>
</div>


<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>13/13</span>
</div>


<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>79/79</span>
</div>


</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a>
<a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a>
<a name='L231'></a><a href='#L231'>231</a>
<a name='L232'></a><a href='#L232'>232</a>
<a name='L233'></a><a href='#L233'>233</a>
<a name='L234'></a><a href='#L234'>234</a>
<a name='L235'></a><a href='#L235'>235</a>
<a name='L236'></a><a href='#L236'>236</a>
<a name='L237'></a><a href='#L237'>237</a>
<a name='L238'></a><a href='#L238'>238</a>
<a name='L239'></a><a href='#L239'>239</a>
<a name='L240'></a><a href='#L240'>240</a>
<a name='L241'></a><a href='#L241'>241</a>
<a name='L242'></a><a href='#L242'>242</a>
<a name='L243'></a><a href='#L243'>243</a>
<a name='L244'></a><a href='#L244'>244</a>
<a name='L245'></a><a href='#L245'>245</a>
<a name='L246'></a><a href='#L246'>246</a>
<a name='L247'></a><a href='#L247'>247</a>
<a name='L248'></a><a href='#L248'>248</a>
<a name='L249'></a><a href='#L249'>249</a>
<a name='L250'></a><a href='#L250'>250</a>
<a name='L251'></a><a href='#L251'>251</a>
<a name='L252'></a><a href='#L252'>252</a>
<a name='L253'></a><a href='#L253'>253</a>
<a name='L254'></a><a href='#L254'>254</a>
<a name='L255'></a><a href='#L255'>255</a>
<a name='L256'></a><a href='#L256'>256</a>
<a name='L257'></a><a href='#L257'>257</a>
<a name='L258'></a><a href='#L258'>258</a>
<a name='L259'></a><a href='#L259'>259</a>
<a name='L260'></a><a href='#L260'>260</a>
<a name='L261'></a><a href='#L261'>261</a>
<a name='L262'></a><a href='#L262'>262</a>
<a name='L263'></a><a href='#L263'>263</a>
<a name='L264'></a><a href='#L264'>264</a>
<a name='L265'></a><a href='#L265'>265</a>
<a name='L266'></a><a href='#L266'>266</a>
<a name='L267'></a><a href='#L267'>267</a>
<a name='L268'></a><a href='#L268'>268</a>
<a name='L269'></a><a href='#L269'>269</a>
<a name='L270'></a><a href='#L270'>270</a>
<a name='L271'></a><a href='#L271'>271</a>
<a name='L272'></a><a href='#L272'>272</a>
<a name='L273'></a><a href='#L273'>273</a>
<a name='L274'></a><a href='#L274'>274</a>
<a name='L275'></a><a href='#L275'>275</a>
<a name='L276'></a><a href='#L276'>276</a>
<a name='L277'></a><a href='#L277'>277</a>
<a name='L278'></a><a href='#L278'>278</a>
<a name='L279'></a><a href='#L279'>279</a>
<a name='L280'></a><a href='#L280'>280</a>
<a name='L281'></a><a href='#L281'>281</a>
<a name='L282'></a><a href='#L282'>282</a>
<a name='L283'></a><a href='#L283'>283</a>
<a name='L284'></a><a href='#L284'>284</a>
<a name='L285'></a><a href='#L285'>285</a>
<a name='L286'></a><a href='#L286'>286</a>
<a name='L287'></a><a href='#L287'>287</a>
<a name='L288'></a><a href='#L288'>288</a>
<a name='L289'></a><a href='#L289'>289</a>
<a name='L290'></a><a href='#L290'>290</a>
<a name='L291'></a><a href='#L291'>291</a>
<a name='L292'></a><a href='#L292'>292</a>
<a name='L293'></a><a href='#L293'>293</a>
<a name='L294'></a><a href='#L294'>294</a>
<a name='L295'></a><a href='#L295'>295</a>
<a name='L296'></a><a href='#L296'>296</a>
<a name='L297'></a><a href='#L297'>297</a>
<a name='L298'></a><a href='#L298'>298</a>
<a name='L299'></a><a href='#L299'>299</a>
<a name='L300'></a><a href='#L300'>300</a>
<a name='L301'></a><a href='#L301'>301</a>
<a name='L302'></a><a href='#L302'>302</a>
<a name='L303'></a><a href='#L303'>303</a>
<a name='L304'></a><a href='#L304'>304</a>
<a name='L305'></a><a href='#L305'>305</a>
<a name='L306'></a><a href='#L306'>306</a>
<a name='L307'></a><a href='#L307'>307</a>
<a name='L308'></a><a href='#L308'>308</a>
<a name='L309'></a><a href='#L309'>309</a>
<a name='L310'></a><a href='#L310'>310</a>
<a name='L311'></a><a href='#L311'>311</a>
<a name='L312'></a><a href='#L312'>312</a>
<a name='L313'></a><a href='#L313'>313</a>
<a name='L314'></a><a href='#L314'>314</a>
<a name='L315'></a><a href='#L315'>315</a>
<a name='L316'></a><a href='#L316'>316</a>
<a name='L317'></a><a href='#L317'>317</a>
<a name='L318'></a><a href='#L318'>318</a>
<a name='L319'></a><a href='#L319'>319</a>
<a name='L320'></a><a href='#L320'>320</a>
<a name='L321'></a><a href='#L321'>321</a>
<a name='L322'></a><a href='#L322'>322</a>
<a name='L323'></a><a href='#L323'>323</a>
<a name='L324'></a><a href='#L324'>324</a>
<a name='L325'></a><a href='#L325'>325</a>
<a name='L326'></a><a href='#L326'>326</a>
<a name='L327'></a><a href='#L327'>327</a>
<a name='L328'></a><a href='#L328'>328</a>
<a name='L329'></a><a href='#L329'>329</a>
<a name='L330'></a><a href='#L330'>330</a>
<a name='L331'></a><a href='#L331'>331</a>
<a name='L332'></a><a href='#L332'>332</a>
<a name='L333'></a><a href='#L333'>333</a>
<a name='L334'></a><a href='#L334'>334</a>
<a name='L335'></a><a href='#L335'>335</a>
<a name='L336'></a><a href='#L336'>336</a>
<a name='L337'></a><a href='#L337'>337</a>
<a name='L338'></a><a href='#L338'>338</a>
<a name='L339'></a><a href='#L339'>339</a>
<a name='L340'></a><a href='#L340'>340</a>
<a name='L341'></a><a href='#L341'>341</a>
<a name='L342'></a><a href='#L342'>342</a>
<a name='L343'></a><a href='#L343'>343</a>
<a name='L344'></a><a href='#L344'>344</a>
<a name='L345'></a><a href='#L345'>345</a>
<a name='L346'></a><a href='#L346'>346</a>
<a name='L347'></a><a href='#L347'>347</a>
<a name='L348'></a><a href='#L348'>348</a>
<a name='L349'></a><a href='#L349'>349</a>
<a name='L350'></a><a href='#L350'>350</a>
<a name='L351'></a><a href='#L351'>351</a>
<a name='L352'></a><a href='#L352'>352</a>
<a name='L353'></a><a href='#L353'>353</a>
<a name='L354'></a><a href='#L354'>354</a>
<a name='L355'></a><a href='#L355'>355</a>
<a name='L356'></a><a href='#L356'>356</a>
<a name='L357'></a><a href='#L357'>357</a>
<a name='L358'></a><a href='#L358'>358</a>
<a name='L359'></a><a href='#L359'>359</a>
<a name='L360'></a><a href='#L360'>360</a>
<a name='L361'></a><a href='#L361'>361</a>
<a name='L362'></a><a href='#L362'>362</a>
<a name='L363'></a><a href='#L363'>363</a>
<a name='L364'></a><a href='#L364'>364</a>
<a name='L365'></a><a href='#L365'>365</a>
<a name='L366'></a><a href='#L366'>366</a>
<a name='L367'></a><a href='#L367'>367</a>
<a name='L368'></a><a href='#L368'>368</a>
<a name='L369'></a><a href='#L369'>369</a>
<a name='L370'></a><a href='#L370'>370</a>
<a name='L371'></a><a href='#L371'>371</a>
<a name='L372'></a><a href='#L372'>372</a>
<a name='L373'></a><a href='#L373'>373</a>
<a name='L374'></a><a href='#L374'>374</a>
<a name='L375'></a><a href='#L375'>375</a>
<a name='L376'></a><a href='#L376'>376</a>
<a name='L377'></a><a href='#L377'>377</a>
<a name='L378'></a><a href='#L378'>378</a>
<a name='L379'></a><a href='#L379'>379</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">49x</span>
<span class="cline-any cline-yes">49x</span>
<span class="cline-any cline-yes">49x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">49x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">145x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">145x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">145x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">140x</span>
<span class="cline-any cline-yes">140x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">140x</span>
<span class="cline-any cline-yes">141x</span>
<span class="cline-any cline-yes">141x</span>
<span class="cline-any cline-yes">141x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">141x</span>
<span class="cline-any cline-yes">141x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">141x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">141x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">141x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">140x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">73x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">140x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useEffect, useRef } from 'react';
import { Dimensions } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
interpolate,
Extrapolation,
type SharedValue,
} from 'react-native-reanimated';
import Svg, {
Circle,
Defs,
LinearGradient as SvgLinearGradient,
Stop,
} from 'react-native-svg';
&nbsp;
import { scheduleOnRN } from 'react-native-worklets';
&nbsp;
import { View, type ImageSourcePropType } from 'react-native';
&nbsp;
// Type for pan gesture event payload
type PanGestureEvent = {
translationX: number;
velocityX: number;
};
&nbsp;
const SCREEN_WIDTH = Dimensions.get('window').width;
&nbsp;
// Default values
const DEFAULT_ITEM_SIZE = 192;
const DEFAULT_ITEM_SPACING = 200;
const DEFAULT_MIN_SCALE = 0.7;
const DEFAULT_MAX_SCALE = 1.0;
const DEFAULT_GRADIENT_COLORS = ['orange', 'blue', 'violet'];
const DEFAULT_GRADIENT_ID = 'carousel-gradient';
&nbsp;
// Animation configuration
const SPRING_DAMPING = 20;
const SPRING_STIFFNESS = 90;
const VELOCITY_THRESHOLD = 500;
&nbsp;
// Opacity values for item interpolation
const MAX_OPACITY = 1;
const MIN_OPACITY = 0.6;
&nbsp;
// Gradient border configuration
const GRADIENT_BORDER_PADDING = 8;
const GRADIENT_STROKE_WIDTH = 4;
const GRADIENT_RADIUS_OFFSET = 2;
&nbsp;
// Layout spacing
const DETAIL_VIEW_MARGIN_TOP = 16;
&nbsp;
// Gradient SVG configuration
const GRADIENT_STOP_OPACITY = '1';
const GRADIENT_START_X = '0%';
const GRADIENT_START_Y = '0%';
const GRADIENT_END_X = '100%';
const GRADIENT_END_Y = '100%';
&nbsp;
// Type for image source - can be a URL string or local image source
export type ImageSource = string | ImageSourcePropType;
&nbsp;
// Base type that items must extend - requires an image property
export type CarouselItemBase = {
image: ImageSource;
};
&nbsp;
type CarouselProps&lt;T extends CarouselItemBase&gt; = {
items: T[];
selectedItem: T;
onItemChange: (item: T) =&gt; void;
getItemId: (item: T) =&gt; string;
renderItem: (
item: T,
index: number,
translateX: SharedValue&lt;number&gt;
) =&gt; React.ReactNode;
renderDetail?: (item: T) =&gt; React.ReactNode;
itemSize?: number;
itemSpacing?: number;
minScale?: number;
maxScale?: number;
showGradientBorder?: boolean;
gradientColors?: string[];
gradientId?: string;
};
&nbsp;
type GradientBorderProps = {
itemSize: number;
gradientColors: string[];
gradientId: string;
};
&nbsp;
function GradientBorder({
itemSize,
gradientColors,
gradientId,
}: GradientBorderProps) {
const gradientSize = itemSize + GRADIENT_BORDER_PADDING;
const gradientCenter = gradientSize / 2;
const gradientRadius = gradientCenter - GRADIENT_RADIUS_OFFSET;
&nbsp;
return (
&lt;View
style={{
position: 'absolute',
width: gradientSize,
height: gradientSize,
}}
pointerEvents="none"
&gt;
&lt;Svg width={gradientSize} height={gradientSize}&gt;
&lt;Defs&gt;
&lt;SvgLinearGradient
id={gradientId}
x1={GRADIENT_START_X}
y1={GRADIENT_START_Y}
x2={GRADIENT_END_X}
y2={GRADIENT_END_Y}
&gt;
{gradientColors.map((color, index) =&gt; {
const colorCount = gradientColors.length;
const offset =
colorCount === 1
? '0%'
: `${(index / (colorCount - 1)) * 100}%`;
return (
&lt;Stop
key={color}
offset={offset}
stopColor={color}
stopOpacity={GRADIENT_STOP_OPACITY}
/&gt;
);
})}
&lt;/SvgLinearGradient&gt;
&lt;/Defs&gt;
&lt;Circle
cx={gradientCenter}
cy={gradientCenter}
r={gradientRadius}
stroke={`url(#${gradientId})`}
strokeWidth={GRADIENT_STROKE_WIDTH}
fill="none"
/&gt;
&lt;/Svg&gt;
&lt;/View&gt;
);
}
&nbsp;
type CarouselItemProps&lt;T extends CarouselItemBase&gt; = {
item: T;
index: number;
translateX: SharedValue&lt;number&gt;;
itemSize: number;
itemSpacing: number;
minScale: number;
maxScale: number;
renderItem: (
item: T,
index: number,
translateX: SharedValue&lt;number&gt;
) =&gt; React.ReactNode;
};
&nbsp;
function CarouselItem&lt;T extends CarouselItemBase&gt;({
item,
index,
translateX,
itemSize,
itemSpacing,
minScale,
maxScale,
renderItem,
}: CarouselItemProps&lt;T&gt;) {
const CENTER_X = SCREEN_WIDTH / 2;
const CENTER_OFFSET = CENTER_X - itemSize / 2;
&nbsp;
const animatedStyle = useAnimatedStyle(() =&gt; {
const basePosition = CENTER_OFFSET;
const offset = index * itemSpacing;
const position = basePosition + translateX.value + offset;
&nbsp;
const distanceFromCenter = Math.abs(position + itemSize / 2 - CENTER_X);
const maxDistance = itemSpacing;
&nbsp;
const scale = interpolate(
distanceFromCenter,
[0, maxDistance],
[maxScale, minScale],
Extrapolation.CLAMP
);
&nbsp;
const opacity = interpolate(
distanceFromCenter,
[0, maxDistance],
[MAX_OPACITY, MIN_OPACITY],
Extrapolation.CLAMP
);
&nbsp;
return {
transform: [{ translateX: position }, { scale }],
opacity,
};
});
&nbsp;
return (
&lt;Animated.View
style={[
{
position: 'absolute',
justifyContent: 'center',
alignItems: 'center',
width: itemSize,
height: itemSize,
left: 0,
top: 0,
},
animatedStyle,
]}
&gt;
{renderItem(item, index, translateX)}
&lt;/Animated.View&gt;
);
}
&nbsp;
export function Carousel&lt;T extends CarouselItemBase&gt;({
items,
selectedItem,
onItemChange,
getItemId,
renderItem,
renderDetail,
itemSize = DEFAULT_ITEM_SIZE,
itemSpacing = DEFAULT_ITEM_SPACING,
minScale = DEFAULT_MIN_SCALE,
maxScale = DEFAULT_MAX_SCALE,
showGradientBorder = true,
gradientColors = DEFAULT_GRADIENT_COLORS,
gradientId = DEFAULT_GRADIENT_ID,
}: CarouselProps&lt;T&gt;) {
const initialIndex = items.findIndex(
(item) =&gt; getItemId(item) === getItemId(selectedItem)
);
const translateX = useSharedValue(-initialIndex * itemSpacing);
const currentIndex = useSharedValue(initialIndex &gt;= 0 ? initialIndex : 0);
const previousItemId = useRef(getItemId(selectedItem));
&nbsp;
useEffect(() =&gt; {
const currentItemId = getItemId(selectedItem);
if (previousItemId.current !== currentItemId) {
previousItemId.current = currentItemId;
const index = items.findIndex(
(item) =&gt; getItemId(item) === currentItemId
);
if (index !== -1) {
currentIndex.value = index;
translateX.value = withSpring(-index * itemSpacing, {
damping: SPRING_DAMPING,
stiffness: SPRING_STIFFNESS,
});
}
}
}, [selectedItem, items, currentIndex, translateX, getItemId, itemSpacing]);
&nbsp;
const snapToIndex = (index: number) =&gt; {
'worklet';
const clampedIndex = Math.max(0, Math.min(index, items.length - 1));
translateX.value = withSpring(-clampedIndex * itemSpacing, {
damping: SPRING_DAMPING,
stiffness: SPRING_STIFFNESS,
});
currentIndex.value = clampedIndex;
};
&nbsp;
const updateSelectedItem = (index: number) =&gt; {
const clampedIndex = Math.max(0, Math.min(index, items.length - 1));
const item = items[clampedIndex];
if (item &amp;&amp; getItemId(item) !== getItemId(selectedItem)) {
onItemChange(item);
}
};
&nbsp;
const panGesture = Gesture.Pan()
.onUpdate((event: PanGestureEvent) =&gt; {
const newTranslateX =
-currentIndex.value * itemSpacing + event.translationX;
const minTranslateX = -(items.length - 1) * itemSpacing;
const maxTranslateX = 0;
&nbsp;
translateX.value = Math.max(
minTranslateX,
Math.min(maxTranslateX, newTranslateX)
);
})
.onEnd((event: PanGestureEvent) =&gt; {
const currentTranslateX = translateX.value;
const velocity = event.velocityX;
&nbsp;
let targetIndex = Math.round(-currentTranslateX / itemSpacing);
&nbsp;
if (Math.abs(velocity) &gt; VELOCITY_THRESHOLD) {
if (velocity &lt; 0) {
targetIndex = Math.min(targetIndex + 1, items.length - 1);
} else {
targetIndex = Math.max(targetIndex - 1, 0);
}
}
&nbsp;
snapToIndex(targetIndex);
scheduleOnRN(updateSelectedItem, targetIndex);
});
&nbsp;
return (
&lt;View
style={{
justifyContent: 'center',
alignItems: 'center',
width: '100%',
}}
&gt;
&lt;View
style={{
height: itemSize,
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
}}
&gt;
&lt;GestureDetector gesture={panGesture}&gt;
&lt;Animated.View
style={{
position: 'absolute',
width: SCREEN_WIDTH,
height: itemSize,
}}
&gt;
{items.map((item, index) =&gt; (
&lt;CarouselItem
key={getItemId(item)}
item={item}
index={index}
translateX={translateX}
itemSize={itemSize}
itemSpacing={itemSpacing}
minScale={minScale}
maxScale={maxScale}
renderItem={renderItem}
/&gt;
))}
&lt;/Animated.View&gt;
&lt;/GestureDetector&gt;
{showGradientBorder &amp;&amp; (
&lt;GradientBorder
itemSize={itemSize}
gradientColors={gradientColors}
gradientId={gradientId}
/&gt;
)}
&lt;/View&gt;
{renderDetail &amp;&amp; (
&lt;View
style={{
justifyContent: 'center',
alignItems: 'center',
marginTop: DETAIL_VIEW_MARGIN_TOP,
}}
&gt;
{renderDetail(selectedItem)}
&lt;/View&gt;
)}
&lt;/View&gt;
);
}
&nbsp;</pre></td></tr></table></pre>

<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2025-11-27T14:24:20.221Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Coverage reports should not be committed to version control.

The coverage/ directory contains auto-generated test coverage artifacts from Istanbul. These files are regenerated on every test run and should be excluded from version control to:

  • Avoid repository bloat
  • Prevent merge conflicts
  • Keep commits focused on source code changes

Add the coverage directory to .gitignore:

+# Coverage reports
+coverage/

Then remove the committed coverage files:

git rm -r --cached coverage/
🤖 Prompt for AI Agents
In coverage/lcov-report/index.tsx.html lines 1-1219: the coverage/ artifacts are
committed but should be ignored; update .gitignore to include the coverage/
directory, remove the tracked coverage files from git's index (stop tracking the
directory) and commit the .gitignore change and the removal so the generated
coverage files are no longer stored in the repository.

Comment thread example/src/App.tsx
Comment thread jest-setup.js
Comment on lines +39 to +60
const { Dimensions } = require('react-native');
const mockDimensions = {
window: {
width: 375,
height: 812,
},
};

jest.spyOn(Dimensions, 'get').mockReturnValue(mockDimensions);

// Mock react-native-worklets scheduleOnRN to execute synchronously in tests
jest.mock('react-native-worklets', () => {
const actualModule = jest.requireActual('react-native-worklets');
return {
...actualModule,
scheduleOnRN: jest.fn((fn, ...args) => {
// Execute synchronously in tests
if (typeof fn === 'function') {
fn(...args);
}
}),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

React Native Dimensions.get() API documentation

💡 Result:

Dimensions.get(dim) — returns a ScaledSize for 'window' or 'screen'. Use: const {width, height} = Dimensions.get('window'); Signature: static get(dim: 'window' | 'screen'): ScaledSize. ScaledSize has { width, height, scale, fontScale }. Note: useWindowDimensions() is the preferred hook (it updates on size changes); on Android 'window' may exclude status/navigation bar size. (reactnative.dev)

Citations:


🏁 Script executed:

# Find and read jest-setup.js file
fd jest-setup.js --type f -x cat -n {}

Repository: fyndx/react-native-swipe-carousel

Length of output: 2062


🏁 Script executed:

# Search for Dimensions usage in the codebase to see how it's being called
rg -n "Dimensions\.get" --type js --type ts -C 3

Repository: fyndx/react-native-swipe-carousel

Length of output: 323


Fix Dimensions.get mock to match React Native API behavior

The current mock is incorrect and will cause runtime failures in tests. According to the React Native documentation, Dimensions.get('window') returns an object where width and height are top-level properties: { width, height, scale, fontScale }.

The codebase uses this API (e.g., src/index.tsx:29):

const SCREEN_WIDTH = Dimensions.get('window').width;

However, the current mock returns:

mockDimensions = { window: { width: 375, height: 812 } }
jest.spyOn(Dimensions, 'get').mockReturnValue(mockDimensions);

This means Dimensions.get('window') returns { window: { width: 375, height: 812 } }, so Dimensions.get('window').width evaluates to undefined instead of 375, breaking the code in tests.

Fix: Implement the mock to accept the dimension parameter and return the correct shape:

-const mockDimensions = {
-  window: {
-    width: 375,
-    height: 812,
-  },
-};
-
-jest.spyOn(Dimensions, 'get').mockReturnValue(mockDimensions);
+jest.spyOn(Dimensions, 'get').mockImplementation((dim) => {
+  const dimensions = {
+    width: 375,
+    height: 812,
+    scale: 1,
+    fontScale: 1,
+  };
+  if (dim === 'screen') {
+    return dimensions;
+  }
+  return dimensions; // window dimensions
+});
🤖 Prompt for AI Agents
In jest-setup.js around lines 39 to 60, the Dimensions.get mock returns an
object with a nested window key which makes Dimensions.get('window').width
undefined; change the mock to accept the dimension key parameter and return the
correct shape for that key (i.e., when called with 'window' return {width: 375,
height: 812, scale: 1, fontScale: 1}, and for other keys return an appropriate
object or undefined) by replacing mockReturnValue with a mockImplementation that
inspects the argument and returns the top-level width/height/scale/fontScale
object to match the React Native API.

Comment thread src/index.tsx
Comment on lines +245 to +250
const initialIndex = items.findIndex(
(item) => getItemId(item) === getItemId(selectedItem)
);
const translateX = useSharedValue(-initialIndex * itemSpacing);
const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0);
const previousItemId = useRef(getItemId(selectedItem));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent handling when selectedItem is not found in items.

When selectedItem is not in the items array:

  • initialIndex = -1
  • translateX = -(-1) × itemSpacing = +itemSpacing (positions carousel incorrectly)
  • currentIndex = 0 (correctly clamped)

This creates a mismatch between the animation position and the tracked index.

Apply consistent clamping:

  const initialIndex = items.findIndex(
    (item) => getItemId(item) === getItemId(selectedItem)
  );
-  const translateX = useSharedValue(-initialIndex * itemSpacing);
-  const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0);
+  const validInitialIndex = initialIndex >= 0 ? initialIndex : 0;
+  const translateX = useSharedValue(-validInitialIndex * itemSpacing);
+  const currentIndex = useSharedValue(validInitialIndex);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const initialIndex = items.findIndex(
(item) => getItemId(item) === getItemId(selectedItem)
);
const translateX = useSharedValue(-initialIndex * itemSpacing);
const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0);
const previousItemId = useRef(getItemId(selectedItem));
const initialIndex = items.findIndex(
(item) => getItemId(item) === getItemId(selectedItem)
);
const validInitialIndex = initialIndex >= 0 ? initialIndex : 0;
const translateX = useSharedValue(-validInitialIndex * itemSpacing);
const currentIndex = useSharedValue(validInitialIndex);
const previousItemId = useRef(getItemId(selectedItem));
🤖 Prompt for AI Agents
In src/index.tsx around lines 245 to 250, initialIndex can be -1 when
selectedItem isn't found which causes translateX to be computed from
-initialIndex while currentIndex is clamped to 0, so clamp the index once and
reuse it: compute a clampedIndex = initialIndex >= 0 ? initialIndex : 0 (or
Math.max(initialIndex, 0)) and use clampedIndex for translateX and currentIndex
(and any other places that rely on the initial index) so animation position and
tracked index remain consistent.

- Removed coverage reports and related files from the repository to streamline project structure.
- Updated .gitignore to include coverage directory for future exclusions.
…ling

- Introduced a new GitHub Actions workflow for coverage reporting on pull requests.
- Enhanced error handling in the fetchImages function to throw an error for non-OK responses.
- Updated key generation in the GradientBorder component for better uniqueness.
- Adjusted dependencies in the Carousel component's useEffect to prevent exhaustive-deps warnings.
- Added assertions in tests to ensure onItemChange is not called under specific conditions.
@codeant-ai

codeant-ai Bot commented Nov 29, 2025

Copy link
Copy Markdown

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XXL This PR changes 1000+ lines, ignoring generated files labels Nov 29, 2025
@codeant-ai

codeant-ai Bot commented Nov 29, 2025

Copy link
Copy Markdown

CodeAnt AI Incremental review completed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
src/index.tsx (1)

245-249: Clamp initialIndex before computing translateX to prevent misalignment.

When selectedItem is not found in items, initialIndex becomes -1, causing:

  • translateX = -(-1) × itemSpacing = +itemSpacing (positions carousel incorrectly to the right)
  • currentIndex = 0 (correctly clamped)

This mismatch causes the carousel to render at the wrong initial position until the next update.

Apply this fix:

  const initialIndex = items.findIndex(
    (item) => getItemId(item) === getItemId(selectedItem)
  );
-  const translateX = useSharedValue(-initialIndex * itemSpacing);
-  const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0);
+  const validInitialIndex = Math.max(0, initialIndex);
+  const translateX = useSharedValue(-validInitialIndex * itemSpacing);
+  const currentIndex = useSharedValue(validInitialIndex);
🧹 Nitpick comments (1)
example/src/App.tsx (1)

33-36: Consider simplifying the data transformation.

The image field is mapped from photo.download_url, but download_url is already available on PicsumPhoto. If the Carousel can accept download_url directly, you could eliminate this transformation. If the separate image field is intentional for API consistency, consider adding a comment explaining the normalization.

If the Carousel's renderItem can reference item.download_url instead of item.image, you could simplify:

-        const transformedItems: PicsumPhoto[] = data.map((photo) => ({
-          ...photo,
-          image: photo.download_url,
-        }));
+        const transformedItems: PicsumPhoto[] = data;

And update line 75 to use item.download_url instead of item.image.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62a75d8 and 5cec350.

📒 Files selected for processing (5)
  • .github/workflows/coverage.yml (1 hunks)
  • .gitignore (1 hunks)
  • example/src/App.tsx (2 hunks)
  • src/__tests__/index.test.tsx (1 hunks)
  • src/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/__tests__/index.test.tsx (1)
src/index.tsx (2)
  • CarouselItemBase (67-69)
  • Carousel (230-380)
src/index.tsx (1)
jest-setup.js (5)
  • React (6-6)
  • Svg (21-21)
  • Defs (23-23)
  • Stop (25-25)
  • Circle (22-22)
example/src/App.tsx (1)
src/index.tsx (1)
  • Carousel (230-380)
🪛 actionlint (1.7.9)
.github/workflows/coverage.yml

7-7: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 GitHub Check: lint
src/__tests__/index.test.tsx

[warning] 725-725:
Test has no assertions


[warning] 644-644:
Test has no assertions


[warning] 619-619:
Test has no assertions


[warning] 594-594:
Test has no assertions


[warning] 569-569:
Test has no assertions

src/index.tsx

[warning] 108-108:
Inline style: { position: 'absolute' }

example/src/App.tsx

[warning] 86-86:
Inline style: { justifyContent: 'center', alignItems: 'center' }


[warning] 63-63:
Inline style: { flex: 1 }


[warning] 55-55:
Inline style: { flex: 1 }

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (2)
example/src/App.tsx (1)

28-30: LGTM! Response status check added.

The HTTP status check has been added as suggested in the previous review, improving error handling.

src/index.tsx (1)

132-132: LGTM! Gradient key issue resolved.

The composite key ${color}-${index} correctly addresses the previous concern about duplicate color keys. This ensures stable, unique keys for React reconciliation.

coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Update actions/checkout to v4.

The v3 action is too old and will fail on newer GitHub-hosted runners.

Apply this diff:

-            - uses: actions/checkout@v3
+            - uses: actions/checkout@v4
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v3
- uses: actions/checkout@v4
🧰 Tools
🪛 actionlint (1.7.9)

7-7: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
In .github/workflows/coverage.yml around line 7, update the actions/checkout
step from actions/checkout@v3 to actions/checkout@v4; edit the workflow file to
replace the version tag so the checkout action uses v4 (or the latest stable
major) to ensure compatibility with newer GitHub-hosted runners.

Comment on lines +569 to +592
it('should handle pan gesture with negative translationX', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;

render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);

const gesture = getByGestureTestId('carousel-pan-gesture');

fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: -100 },
{ state: State.END, translationX: -100 },
]);

jest.advanceTimersByTime(1000);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add assertion to verify gesture behavior.

This test fires a pan gesture with negative translationX but doesn't assert any observable outcome. Without assertions, the test will pass even if the gesture logic breaks.

Add an assertion to verify the expected behavior:

      jest.advanceTimersByTime(1000);
+
+      // Swiping left from the first item should move to the next item
+      expect(mockOnItemChange).toHaveBeenCalledWith(items[1]);
🧰 Tools
🪛 GitHub Check: lint

[warning] 569-569:
Test has no assertions

🤖 Prompt for AI Agents
In src/__tests__/index.test.tsx around lines 569 to 592, the test fires a pan
gesture with negative translationX but contains no assertions; add an assertion
after advancing timers to verify the observable outcome (e.g., expect
mockOnItemChange to have been called once with the next item in the list,
items[1], or otherwise assert the selected item changed appropriately and the
handler was invoked the expected number of times).

Comment on lines +594 to +617
it('should handle pan gesture clamping to minTranslateX', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;

render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);

const gesture = getByGestureTestId('carousel-pan-gesture');

fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: -1000 },
{ state: State.END, translationX: -1000 },
]);

jest.advanceTimersByTime(1000);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add assertion to verify clamping behavior.

This test fires an extreme left swipe but doesn't verify that the carousel correctly clamps to the last item.

Add an assertion:

      jest.advanceTimersByTime(1000);
+
+      // Should clamp to the last item
+      expect(mockOnItemChange).toHaveBeenCalledWith(items[2]);
🧰 Tools
🪛 GitHub Check: lint

[warning] 594-594:
Test has no assertions

🤖 Prompt for AI Agents
In src/__tests__/index.test.tsx around lines 594 to 617, the test triggers an
extreme left swipe but never asserts that the carousel clamped to the last item;
add an assertion that verifies clamping by expecting the onItemChange handler to
have been called with the last item's id (use mockOnItemChange and mockGetItemId
on the last item), or alternatively assert the carousel's transform/translateX
equals the known minTranslateX value after timers advance, ensuring the test
verifies the carousel ended at the clamped position.

Comment on lines +619 to +642
it('should handle pan gesture clamping to maxTranslateX', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;

render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);

const gesture = getByGestureTestId('carousel-pan-gesture');

fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: 1000 },
{ state: State.END, translationX: 1000 },
]);

jest.advanceTimersByTime(1000);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add assertion to verify clamping behavior.

This test fires an extreme right swipe from the first item but doesn't verify that the carousel correctly stays at the first item (clamping to maxTranslateX).

Add an assertion:

      jest.advanceTimersByTime(1000);
+
+      // Should stay on first item (already at boundary)
+      expect(mockOnItemChange).not.toHaveBeenCalled();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should handle pan gesture clamping to maxTranslateX', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;
render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);
const gesture = getByGestureTestId('carousel-pan-gesture');
fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: 1000 },
{ state: State.END, translationX: 1000 },
]);
jest.advanceTimersByTime(1000);
});
it('should handle pan gesture clamping to maxTranslateX', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;
render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);
const gesture = getByGestureTestId('carousel-pan-gesture');
fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: 1000 },
{ state: State.END, translationX: 1000 },
]);
jest.advanceTimersByTime(1000);
// Should stay on first item (already at boundary)
expect(mockOnItemChange).not.toHaveBeenCalled();
});
🧰 Tools
🪛 GitHub Check: lint

[warning] 619-619:
Test has no assertions

🤖 Prompt for AI Agents
In src/__tests__/index.test.tsx around lines 619 to 642, the test sends an
extreme right swipe but contains no assertion verifying clamping; add a
post-gesture assertion to confirm the carousel remained clamped at the first
item. After advancing timers, assert that the onItemChange callback was not
called (or that the selected item is still the first item), or assert the
carousel's computed translateX equals the expected maxTranslateX value for the
first item so the test verifies clamping behavior.

Comment on lines +644 to +667
it('should handle pan gesture end with low velocity', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;

render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);

const gesture = getByGestureTestId('carousel-pan-gesture');

fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: -200 },
{ state: State.END, translationX: -200, velocityX: 100 },
]);

jest.advanceTimersByTime(1000);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add assertion to verify low-velocity snap behavior.

This test verifies gesture execution with low velocity but doesn't assert the snap outcome.

Add an assertion to verify which item is selected after the snap:

      jest.advanceTimersByTime(1000);
+
+      // With low velocity and -200 translation, should snap to nearest item
+      // Expected behavior should match your snap logic (likely items[1])
+      expect(mockOnItemChange).toHaveBeenCalled();
🧰 Tools
🪛 GitHub Check: lint

[warning] 644-644:
Test has no assertions

🤖 Prompt for AI Agents
In src/__tests__/index.test.tsx around lines 644 to 667, the test fires a pan
gesture with low velocity but never asserts the resulting snap selection; add an
assertion after jest.advanceTimersByTime(1000) to verify the selected item by
expecting the onItemChange mock to be called with the next item (e.g.
expect(mockOnItemChange).toHaveBeenCalledWith(items[1])) so the test verifies
the carousel snapped to the expected item after the gesture.

Comment on lines +725 to +748
it('should handle pan gesture end with velocity at boundary', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;

render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);

const gesture = getByGestureTestId('carousel-pan-gesture');

fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: -400 },
{ state: State.END, translationX: -400, velocityX: -600 },
]);

jest.advanceTimersByTime(1000);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add assertion to verify velocity-based navigation at boundary.

This test exercises gesture logic with high velocity at a boundary but doesn't assert the outcome.

Add an assertion:

      jest.advanceTimersByTime(1000);
+
+      // High negative velocity should move to next items
+      expect(mockOnItemChange).toHaveBeenCalled();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should handle pan gesture end with velocity at boundary', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;
render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);
const gesture = getByGestureTestId('carousel-pan-gesture');
fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: -400 },
{ state: State.END, translationX: -400, velocityX: -600 },
]);
jest.advanceTimersByTime(1000);
});
it('should handle pan gesture end with velocity at boundary', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;
render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);
const gesture = getByGestureTestId('carousel-pan-gesture');
fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: -400 },
{ state: State.END, translationX: -400, velocityX: -600 },
]);
jest.advanceTimersByTime(1000);
// High negative velocity should move to next items
expect(mockOnItemChange).toHaveBeenCalled();
});
🧰 Tools
🪛 GitHub Check: lint

[warning] 725-725:
Test has no assertions

🤖 Prompt for AI Agents
In src/__tests__/index.test.tsx around lines 725 to 748, the test simulates a
high-velocity left swipe but lacks an assertion verifying the navigation result;
add an assertion after jest.advanceTimersByTime(1000) that checks the component
invoked the onItemChange handler with the expected next item (e.g., ensure
mockOnItemChange was called once with the id/index of items[1] using
mockGetItemId or the expected value) to confirm velocity-based navigation
occurred at the boundary.

@chakrihacker

Copy link
Copy Markdown
Contributor Author

Demo

Simulator.Screen.Recording.-.iPhone.17.Pro.-.2025-11-30.at.03.08.31.mov

@chakrihacker chakrihacker merged commit 4737f0c into main Nov 29, 2025
4 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review effort 3/5 size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant