Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div class="curved-nav">
  <div class="content-wrapper">
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      A
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      B
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      C
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      D
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      E
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      F
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      G
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      H
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      I
    </div>
    <div
      class="content-item"
      data-vel-plugin="CurvedNavPlugin"
      data-vel-view="contentItem"
    >
      J
    </div>
  </div>
  <div class="nav-container-wrapper">
    <div class="nav-container">
      <div
        class="nav-items"
        data-vel-plugin="CurvedNavPlugin"
        data-vel-view="itemsContainer"
        data-vel-data-active-index="0"
      >
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          A
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          B
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          C
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          D
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          E
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          F
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          G
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          H
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          I
        </div>
        <div
          class="nav-item"
          data-vel-plugin="CurvedNavPlugin"
          data-vel-view="item"
        >
          J
        </div>
      </div>
    </div>
  </div>
</div>
              
            
!

CSS

              
                * {
  box-sizing: border-box;
}
body {
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100vh;
  background: whitesmoke;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  overflow: hidden;
  max-width: 100%;
}

.curved-nav {
  width: 100%;
  height: 100%;
  position: relative;
  will-change: transform, opacity;
}

.curved-nav::before {
  position: absolute;
  content: '';
  width: 100%;
  aspect-ratio: 1;
  border-radius: 20rem;
  background: radial-gradient(
      63.62% 69.52% at 100% 0%,
      rgba(247, 214, 98, 0.8) 0%,
      rgba(247, 214, 98, 0.168) 52.08%,
      rgba(247, 214, 98, 0) 100%
    ),
    linear-gradient(
      208.42deg,
      #f0422a 7.46%,
      rgba(240, 88, 42, 0.18) 42.58%,
      rgba(240, 101, 42, 0) 64.13%
    ),
    radial-gradient(
      114.51% 122.83% at 0% -15.36%,
      #e74f6a 0%,
      rgba(231, 79, 106, 0.22) 66.72%,
      rgba(231, 79, 106, 0) 100%
    ),
    linear-gradient(
      333.95deg,
      rgba(83, 208, 236, 0.85) -7.76%,
      rgba(83, 208, 236, 0.204) 19.67%,
      rgba(138, 137, 190, 0) 35.42%
    ),
    radial-gradient(
      109.15% 148.57% at 4.46% 98.44%,
      #1b3180 0%,
      rgba(27, 49, 128, 0) 100%
    ),
    linear-gradient(141.57deg, #4eadeb 19.08%, rgba(78, 173, 235, 0) 98.72%);
  background-blend-mode: normal, normal, normal, normal, multiply, normal;
  filter: blur(84px);
  will-change: transform;
  backface-visibility: hidden;
  transform: translate3d(0, 0, 0);
}

.nav-container {
  display: flex;
  width: 100%;
  max-width: 375px;
  touch-action: none;
  will-change: transform, opacity;
}

.nav-container-wrapper {
  overflow: hidden;
  padding: 40px 50px;
}

.nav-items {
  display: flex;
  width: 100%;
  --item-size: calc(375px / 5);
}

.nav-item {
  width: var(--item-size);
  height: var(--item-size);
  border-radius: 10px;
  background: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  cursor: pointer;
  color: #222;
  user-select: none;
  -webkit-user-select: none;
  flex-shrink: 0;
  will-change: transform, opacity;
  touch-action: none;
}

.curved-nav {
  width: 100%;
  max-width: 375px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.content-wrapper {
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: 10px;
  width: 250px;
  height: 250px;
  display: flex;
  align-items: center;
  justify-content: center;
  will-change: transform, opacity;
}

.content-item {
  color: white;
  font-size: 300px;
  line-height: 0.7;
  position: absolute;
  will-change: opacity;
}

@media (max-width: 500px) {
  .nav-items {
    --item-size: calc(375px / 6);
  }
  .curved-nav {
    overflow: hidden;
  }
}

              
            
!

JS

              
                // Built with Veloxi: https://veloxijs.com/
// Inspired by: https://twitter.com/huseyingayiran/status/1711294397009080397

const {
    EventBus,
    Events,
    Plugin,
    createApp,
    DragEventPlugin,
    DragEvent
  } = Veloxi

const OFFSET = 10

class SetActiveIndexEvent {
  index
  constructor({ index }) {
    this.index = index
  }
}

class NavItem {
  view
  index
  container
  initialized = false

  constructor(view, index, container) {
    this.view = view
    this.index = index
    this.container = container
    this.view.position.animator.set('dynamic', { speed: 5 })
    this.view.scale.animator.set('dynamic', { speed: 5 })
  }

  init() {
    requestAnimationFrame(() => {
      this.enableTransition()
    })
    this.initialized = true
  }

  enableTransition() {
    this.view.styles.transition = '0.2s opacity linear'
  }

  update() {
    this.updatePosition()
    this.updateOpacity()
    this.updateScale()
  }

  updateWithOffset(offset) {
    const targetSlot = offset > 0 ? this.nextSlot : this.previousSlot
    const percentage = Math.abs(offset / this.container.stepSize)
    this.updatePositionWithOffset(offset, targetSlot, percentage)
    this.updateOpacityWithPercentage(targetSlot, percentage)
    this.updateScaleWithPercentage(targetSlot, percentage)
  }

  updatePositionWithOffset(offset, targetSlot, percentage) {
    const x = this.currentPosition.x
    let y = this.currentPosition.y
    switch (targetSlot) {
      case slotIndex.ACTIVE:
        y -= 10 * percentage
        break
      case slotIndex.FIRST_NEXT:
      case slotIndex.FIRST_PREVIOUS:
        const fromActive = this.slotIndex === slotIndex.ACTIVE
        y += 25 * (fromActive ? 1 : -1) * percentage
        break
      case slotIndex.SECOND_NEXT:
      case slotIndex.SECOND_PREVIOUS:
        const fromFirst = [
          slotIndex.FIRST_NEXT,
          slotIndex.FIRST_PREVIOUS
        ].includes(this.slotIndex)
        y += 40 * percentage * (fromFirst ? 1 : -1)
        break
      default:
        const fromSecond = [
          slotIndex.SECOND_NEXT,
          slotIndex.SECOND_PREVIOUS
        ].includes(this.slotIndex)
        y += 40 * percentage * (fromSecond ? 1 : -1)
    }
    this.view.position.set({ x: x + offset, y })
  }

  updateScaleWithPercentage(targetSlot, percentage) {
    let scale = this.currentScale

    switch (targetSlot) {
      case slotIndex.ACTIVE:
        scale += 0.1 * percentage
        break
      case slotIndex.FIRST_NEXT:
      case slotIndex.FIRST_PREVIOUS:
        const fromActive = this.slotIndex === slotIndex.ACTIVE
        if (fromActive) {
          scale -= 0.1 * percentage
        }
        break
    }
    this.view.scale.set({ x: scale, y: scale })
  }

  updateOpacityWithPercentage(targetSlot, percentage) {
    let opacity = this.currentOpacity
    switch (targetSlot) {
      case slotIndex.ACTIVE:
        opacity += 0.2 * percentage
        break
      case slotIndex.FIRST_PREVIOUS:
      case slotIndex.FIRST_NEXT:
        const fromActive = this.slotIndex === slotIndex.ACTIVE
        opacity += 0.2 * percentage * (fromActive ? -1 : 1)
        break
      case slotIndex.SECOND_PREVIOUS:
      case slotIndex.SECOND_NEXT:
        const fromFirst = [
          slotIndex.FIRST_NEXT,
          slotIndex.FIRST_PREVIOUS
        ].includes(this.slotIndex)
        if (!fromFirst) {
          if (
            this.firstItemIndexInStart === this.index ||
            this.firstItemIndexInEnd === this.index
          ) {
            opacity += 0.2 * percentage * (fromFirst ? -1 : 1)
          } else {
            opacity = 0
          }
        } else {
          opacity += 0.2 * percentage * (fromFirst ? -1 : 1)
        }
        break
      default:
        const fromSecond = [
          slotIndex.SECOND_NEXT,
          slotIndex.SECOND_PREVIOUS
        ].includes(this.slotIndex)
        if (fromSecond) {
          opacity += 0.4 * percentage * (fromSecond ? -1 : 1)
        } else {
          opacity = 0
        }
    }
    this.view.styles.opacity = `${opacity}`
  }

  updatePosition() {
    const shouldAnimate = this.container.shouldAnimateMap.get(this.index)
    const x = this.slotPosition.x
    let y = this.slotPosition.y
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        break
      case slotIndex.FIRST_NEXT:
      case slotIndex.FIRST_PREVIOUS:
        y += 10
        break
      case slotIndex.SECOND_NEXT:
      case slotIndex.SECOND_PREVIOUS:
        y += 40
        break
      default:
        y += 100
    }
    this.view.position.set({ x, y }, shouldAnimate)
  }

  get currentPosition() {
    const x = this.slotPosition.x
    let y = this.slotPosition.y
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        break
      case slotIndex.FIRST_NEXT:
      case slotIndex.FIRST_PREVIOUS:
        y += 10
        break
      case slotIndex.SECOND_NEXT:
      case slotIndex.SECOND_PREVIOUS:
        y += 40
        break
      default:
        y += 80
    }
    return { x, y }
  }

  get currentOpacity() {
    let opacity = 0
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        opacity = 1
        break
      case slotIndex.FIRST_PREVIOUS:
      case slotIndex.FIRST_NEXT:
        opacity = 0.425
        break
      case slotIndex.SECOND_PREVIOUS:
      case slotIndex.SECOND_NEXT:
        opacity = 0.2
        break
    }
    return opacity
  }

  get currentScale() {
    let scale = 0.75
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        scale = 1
        break
      case slotIndex.FIRST_PREVIOUS:
      case slotIndex.FIRST_NEXT:
        scale = 0.75
        break
      case slotIndex.SECOND_PREVIOUS:
      case slotIndex.SECOND_NEXT:
        scale = 0.75
        break
    }
    return scale
  }

  updateOpacity() {
    let opacity = 0
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        opacity = 1
        break
      case slotIndex.FIRST_PREVIOUS:
      case slotIndex.FIRST_NEXT:
        opacity = 0.425
        break
      case slotIndex.SECOND_PREVIOUS:
      case slotIndex.SECOND_NEXT:
        opacity = 0.2
        break
    }

    this.view.styles.opacity = `${opacity}`
  }

  updateScale() {
    let scale = 0.75
    switch (this.slotIndex) {
      case slotIndex.ACTIVE:
        scale = 1
        break
      case slotIndex.FIRST_PREVIOUS:
      case slotIndex.FIRST_NEXT:
        scale = 0.75
        break
      case slotIndex.SECOND_PREVIOUS:
      case slotIndex.SECOND_NEXT:
        scale = 0.75
        break
    }
    this.view.scale.set({ x: scale, y: scale }, this.initialized)
  }

  get nextSlot() {
    return wrapAround(this.slotIndex, Object.keys(slotIndex).length, 1)
  }

  get previousSlot() {
    return wrapAround(this.slotIndex, Object.keys(slotIndex).length, -1)
  }

  get slotPosition() {
    return this.container.getSlotPositionForItemIndex(this.index)
  }

  get activeIndex() {
    return this.container.activeIndex
  }

  get slotIndex() {
    return this.container.getSlotForIndex(this.index)
  }

  getItemIndexForSlot(slot) {
    return this.container.getItemIndeciesForSlot(slot)[0]
  }

  get firstItemIndexInStart() {
    const secondPreviousIndex = this.getItemIndexForSlot(
      slotIndex.SECOND_PREVIOUS
    )
    return wrapAround(secondPreviousIndex, this.container.totalItems, -1)
  }

  get firstItemIndexInEnd() {
    const secondNextItemIndex = this.getItemIndexForSlot(
      slotIndex.SECOND_NEXT
    )
    return wrapAround(secondNextItemIndex, this.container.totalItems, 1)
  }
}

const slotIndex = {
  START: 0,
  SECOND_PREVIOUS: 1,
  FIRST_PREVIOUS: 2,
  ACTIVE: 3,
  FIRST_NEXT: 4,
  SECOND_NEXT: 5,
  END: 6
}

function wrapAround(current, total, amount) {
  return (current + total + amount) % total
}

function flipMap(map) {
  const flippedMap = new Map()

  for (const [key, value] of map) {
    if (!flippedMap.has(value)) {
      flippedMap.set(value, [key])
    } else {
      flippedMap.get(value).push(key)
    }
  }

  return flippedMap
}

class NavContainer {
  plugin
  view
  activeIndex
  shouldAnimateMap = new Map()

  _itemIndexSlotMap = new Map()
  _slotItemIndexMap = new Map()

  constructor(plugin, view) {
    this.plugin = plugin
    this.view = view
    this.activeIndex = this.view.data.activeIndex
      ? parseInt(this.view.data.activeIndex)
      : 0

    for (let index = 0; index < this.totalItems; index++) {
      this.shouldAnimateMap.set(index, false)
    }
  }

  get stepSize() {
    return this.plugin.stepSize
  }

  get itemSize() {
    return this.plugin.itemSize
  }

  get totalItems() {
    return this.plugin.totalItems
  }

  updateWithOffset(offset) {
    const steps = Math.floor(Math.abs(offset / this.stepSize))
    const queue = []
    let currentIndex = this.activeIndex
    for (let step = 0; step < steps; step++) {
      const stepDirection = offset < 0 ? 1 : -1
      const itemIndex = wrapAround(
        currentIndex,
        this.totalItems,
        stepDirection
      )
      queue.push(itemIndex)
      currentIndex = itemIndex
    }
    queue.forEach((itemIndex, index) => {
      setTimeout(() => {
        this.plugin.setActiveIndex(itemIndex)
      }, 100 * index)
    })
  }

  setActiveIndex(newActiveIndex) {
    const previousItemIndexSlot = this.itemIndexSlotMap
    this.activeIndex = newActiveIndex
    this.setItemIndexSlotMap()
    const newItemIndexSlot = this.itemIndexSlotMap

    const visibleSlots = [
      slotIndex.ACTIVE,
      slotIndex.FIRST_PREVIOUS,
      slotIndex.SECOND_PREVIOUS,
      slotIndex.FIRST_NEXT,
      slotIndex.SECOND_NEXT
    ]
    for (let index = 0; index < this.totalItems; index++) {
      const shouldAnimate =
        visibleSlots.includes(previousItemIndexSlot.get(index)) ||
        visibleSlots.includes(newItemIndexSlot.get(index))
      this.shouldAnimateMap.set(index, shouldAnimate)
    }
  }

  get slotItemIndexMap() {
    return this._slotItemIndexMap
  }

  get itemIndexSlotMap() {
    return this._itemIndexSlotMap
  }

  getSlotForIndex(itemIndex) {
    return this.itemIndexSlotMap.get(itemIndex)
  }

  getItemIndeciesForSlot(slot) {
    return this.slotItemIndexMap.get(slot)
  }

  setItemIndexSlotMap() {
    this._itemIndexSlotMap.clear()
    const activeIndex = this.activeIndex

    const firstPreviousIndex = wrapAround(activeIndex, this.totalItems, -1)
    const secondPreviousIndex = wrapAround(activeIndex, this.totalItems, -2)
    const firstNextIndex = wrapAround(activeIndex, this.totalItems, 1)
    const secondNextIndex = wrapAround(activeIndex, this.totalItems, 2)

    for (let index = 0; index < this.totalItems; index++) {
      if (index === activeIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.ACTIVE)
        continue
      }

      if (index === firstPreviousIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.FIRST_PREVIOUS)
        continue
      }

      if (index === secondPreviousIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.SECOND_PREVIOUS)
        continue
      }

      if (index === firstNextIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.FIRST_NEXT)
        continue
      }

      if (index === secondNextIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.SECOND_NEXT)
        continue
      }

      if (index === wrapAround(secondNextIndex, this.totalItems, 1)) {
        this._itemIndexSlotMap.set(index, slotIndex.END)
        continue
      }

      if (index === wrapAround(secondNextIndex, this.totalItems, 2)) {
        this._itemIndexSlotMap.set(index, slotIndex.END)
        continue
      }

      if (index === wrapAround(secondPreviousIndex, this.totalItems, -1)) {
        this._itemIndexSlotMap.set(index, slotIndex.START)
        continue
      }

      if (index === wrapAround(secondPreviousIndex, this.totalItems, -2)) {
        this._itemIndexSlotMap.set(index, slotIndex.START)
        continue
      }

      if (index > activeIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.END)
        continue
      }

      if (index < activeIndex) {
        this._itemIndexSlotMap.set(index, slotIndex.START)
        continue
      }
    }
    this._slotItemIndexMap = flipMap(this.itemIndexSlotMap)
  }

  getSlotPositionForItemIndex(index) {
    const slot = this.itemIndexSlotMap.get(index)
    return this.slotPositions[slot]
  }

  get indicatorPosition() {
    return {
      x: this.view.position.x + this.view.size.width / 2,
      y: this.view.position.y + this.view.size.height / 2
    }
  }

  get slotPositions() {
    const result = []
    // Active Slot
    result[slotIndex.ACTIVE] = {
      x: this.indicatorPosition.x - this.itemSize / 2,
      y: this.indicatorPosition.y - this.itemSize / 2
    }

    result[slotIndex.FIRST_PREVIOUS] = {
      x: result[slotIndex.ACTIVE].x - OFFSET - this.itemSize,
      y: result[slotIndex.ACTIVE].y
    }

    result[slotIndex.SECOND_PREVIOUS] = {
      x: result[slotIndex.FIRST_PREVIOUS].x - this.itemSize,
      y: result[slotIndex.FIRST_PREVIOUS].y
    }

    result[slotIndex.START] = {
      x: result[slotIndex.SECOND_PREVIOUS].x - this.itemSize,
      y: result[slotIndex.SECOND_PREVIOUS].y
    }

    result[slotIndex.FIRST_NEXT] = {
      x: result[slotIndex.ACTIVE].x + OFFSET + this.itemSize,
      y: result[slotIndex.ACTIVE].y
    }

    result[slotIndex.SECOND_NEXT] = {
      x: result[slotIndex.FIRST_NEXT].x + this.itemSize,
      y: result[slotIndex.FIRST_NEXT].y
    }

    result[slotIndex.END] = {
      x: result[slotIndex.SECOND_NEXT].x + this.itemSize,
      y: result[slotIndex.SECOND_NEXT].y
    }

    return result
  }
}

class ContentItem {
  view
  index
  navItemsContainer

  constructor(view, index, navItemsContainer) {
    this.view = view
    this.index = index
    this.navItemsContainer = navItemsContainer
    this.init()
    this.update()
  }

  init() {
    requestAnimationFrame(() => {
      this.enableTransition()
    })
  }

  enableTransition() {
    this.view.styles.transition = '0.2s ease-in-out opacity'
  }

  update() {
    if (this.index !== this.activeIndex) {
      this.hide()
    } else {
      this.show()
    }
  }

  hide() {
    this.view.styles.opacity = '0'
  }

  show() {
    this.view.styles.opacity = '1'
  }

  get activeIndex() {
    return this.navItemsContainer.activeIndex
  }
}

class CurvedNavPlugin extends Plugin {
  static pluginName = 'CurvedNavPlugin'

  items
  itemsContainer

  contentItems

  lastDragOffset = 0
  isDragging = false

  dragEventPlugin = this.useEventPlugin(DragEventPlugin)

  setup() {
    const itemViews = this.getViews('item')
    const itemsContainerView = this.getView('itemsContainer')
    this.itemsContainer = new NavContainer(this, itemsContainerView)

    this.dragEventPlugin.addView(itemsContainerView)
    this.dragEventPlugin.on(DragEvent, this.onDrag.bind(this))

    this.items = itemViews.map(
      (view, index) => new NavItem(view, index, this.itemsContainer)
    )

    this.itemsContainer.setItemIndexSlotMap()

    this.items.forEach((item) => {
      item.update()
      item.init()
    })

    this.contentItems = this.getViews('contentItem').map(
      (view, index) => new ContentItem(view, index, this.itemsContainer)
    )
  }

  onDrag(event) {
    if (event.isDragging) {
      this.isDragging = true
    } else {
      requestAnimationFrame(() => {
        this.isDragging = false
      })
    }
    if (event.isDragging) {
      const diff = Math.abs(event.previousX - event.x)
      const damping = diff > 50 ? 0.2 : 1
      const offset = damping * (event.width + this.lastDragOffset * -1)
      if (Math.abs(offset) >= this.stepSize) {
        this.lastDragOffset = event.width
      }
      this.itemsContainer.updateWithOffset(offset)
      this.items.forEach((item) => {
        item.updateWithOffset(offset)
      })
    } else {
      this.lastDragOffset = 0
      this.items.forEach((item) => {
        item.updateWithOffset(0)
      })
    }
  }

  onDataChanged(data) {
    if (data.dataName === 'activeIndex') {
      const activeIndex = parseInt(data.dataValue)
      this.itemsContainer.setActiveIndex(activeIndex)
      this.items.forEach((item) => item.update())
      this.contentItems.forEach((item) => item.update())
    }
  }

  subscribeToEvents(eventBus) {
    eventBus.subscribeToEvent(Events.PointerClickEvent, ({ target }) => {
      if (this.isDragging) return
      this.items.forEach((item, index) => {
        if (target === item.view.element) {
          this.setActiveIndex(index)
        }
      })
    })
  }

  setActiveIndex(index) {
    this.emit(SetActiveIndexEvent, { index })
  }

  get totalItems() {
    return this.getViews('item').length
  }

  get itemSize() {
    return this.items[0].view.size.width
  }

  get stepSize() {
    return this.itemSize + OFFSET
  }
}

const app = createApp()
app.addPlugin(CurvedNavPlugin)
app.run()

const containerNav = document.querySelector(
  '[data-vel-view="itemsContainer"]'
)
app.onPluginEvent(CurvedNavPlugin, SetActiveIndexEvent, ({ index }) => {
  containerNav.dataset.velDataActiveIndex = `${index}`
})
              
            
!
999px

Console