<template>
  <span ref="root" class="rv" :aria-label="value">
    <template v-for="(char, i) in chars" :key="`${i}-${charKinds[i]}`">
      <span
        v-if="isDigit(char)"
        class="rv-col"
        :style="{ '--y': `-${targetOffset(char)}em`, '--wait': `${i * 28 + 120}ms` }"
        aria-hidden="true"
      >
        <span class="rv-strip" :class="{ go: on }">
          <span v-for="d in len" :key="d" class="rv-d">{{ (d - 1) % 10 }}</span>
        </span>
      </span>
      <span v-else class="rv-static" :class="{ currency: char === '¥', unit: /[a-zA-Z%]/.test(char) }" aria-hidden="true">{{ char }}</span>
    </template>
  </span>
</template>

<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'

const props = defineProps({
  value: { type: String, required: true },
  preRoll: { type: Number, default: 2 }
})

const PRE = props.preRoll * 10
const chars = computed(() => props.value.split(''))
const charKinds = computed(() => chars.value.map((char) => isDigit(char) ? 'digit' : 'static'))
const len = (props.preRoll + 1) * 10

function isDigit(c) { return /\d/.test(c) }
function targetOffset(c) { return PRE + Number(c) }

const on = ref(false)
const root = ref(null)
let observer = null

function roll() {
  if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
    on.value = true
    return
  }

  on.value = false
  nextTick(() => {
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        on.value = true
      })
    })
  })
}

onMounted(() => {
  if (!('IntersectionObserver' in window)) {
    roll()
    return
  }

  observer = new IntersectionObserver((entries) => {
    if (entries.some((entry) => entry.isIntersecting)) {
      roll()
      observer?.disconnect()
    }
  }, { threshold: 0.2 })

  if (root.value) observer.observe(root.value)
})

onBeforeUnmount(() => observer?.disconnect())
watch(() => props.value, roll)
</script>

<style scoped>
.rv {
  display: inline-flex;
  align-items: center;
  line-height: 1;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.rv-col {
  display: inline-block;
  position: relative;
  width: .64em;
  height: 1em;
  line-height: 1;
  overflow: hidden;
  vertical-align: 0;
}

.rv-strip {
  display: block;
  transform: translate3d(0, 0, 0);
  will-change: transform;
}

.rv-strip.go {
  animation: digitRoll 2.35s cubic-bezier(.45, 0, .18, 1) var(--wait, 0ms) both;
}

.rv-d {
  height: 1em;
  display: block;
  text-align: center;
  line-height: 1;
}

.rv-static {
  display: inline-flex;
  align-items: center;
  height: 1em;
  line-height: 1;
}

.rv-static.currency {
  margin-right: .02em;
}

.rv-static.unit {
  margin-left: .04em;
}

@keyframes digitRoll {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    transform: translate3d(0, var(--y, 0), 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .rv-strip {
    animation: none;
    transform: translateY(var(--y, 0));
  }
}
</style>
