<template>
  <div :id="transitionId" ref="slotWrapper" class="cpTransitionSize" :class="componentDynamicClass">
    <slot />
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue'

import { useReducedMotion } from '@/composables/useReducedMotion'

interface Props {
  type?: 'width' | 'height'
}

const props = withDefaults(defineProps<Props>(), { type: 'width' })

const prefersReducedMotion = useReducedMotion()

const slotWrapper = ref()
const slotContent = computed<HTMLElement | null>(() => slotWrapper.value?.children[0])

const isHeight = computed<boolean>(() => props.type === 'height')

const componentDynamicClass = computed(() => {
  return { 'cpTransitionSize--isHeight': isHeight.value }
})

const transitionId = useId()

const updateWrapperSize = async () => {
  if (!slotContent.value) return

  const size = slotContent.value[isHeight.value ? 'clientHeight' : 'clientWidth']
  const property = isHeight.value ? 'min-height' : 'width'

  slotWrapper.value.style[property] = `${size}px`

  const transitionItem = document.getElementById(transitionId)
  if (!transitionItem || !isHeight.value) return

  slotWrapper.value.style.height = 0

  await nextTick()

  transitionItem.addEventListener(
    'transitionend',
    () => {
      if (slotWrapper.value) {
        slotWrapper.value.style.height = 'auto'
      }
    },
    { once: true },
  )
}

const observer = ref<MutationObserver>()

const startObserving = () => {
  if (!slotContent.value || observer.value) return

  observer.value = new MutationObserver(updateWrapperSize)
  observer.value.observe(slotContent.value as HTMLElement, { childList: true, subtree: false })

  updateWrapperSize()
}

const stopObserving = () => {
  observer.value?.disconnect()
  observer.value = undefined

  if (!slotWrapper.value) return

  slotWrapper.value.style.width = ''
  slotWrapper.value.style.height = ''
  slotWrapper.value.style.minHeight = ''
}

watch(prefersReducedMotion, (isReduced) => (isReduced ? stopObserving() : startObserving()))

onMounted(() => {
  if (!prefersReducedMotion.value) startObserving()
})

onBeforeUnmount(() => observer.value?.disconnect())
</script>

<style lang="scss">
.cpTransitionSize {
  display: inline-flex;
  overflow: hidden;
  transition-duration: 350ms;
  transition-property: width;
  transition-timing-function: var(--cp-easing-elastic);
  white-space: nowrap;

  &--isHeight {
    display: block;
    transition-property: min-height;
    transition-timing-function: ease;
  }
}
</style>
