# Dynamic Island

> Composable iPhone Dynamic Island for React and shadcn/ui that morphs between sizes with Motion, for notifications and media controls.

Source: https://www.cult-ui.com/docs/components/dynamic-island

## Example

```tsx title="dynamic-island-demo.tsx"
"use client"

import { createContext, useContext } from "react"
import {
  ArrowUpLeftSquareIcon,
  Loader,
  Mail,
  MessageCircle,
  MousePointerClickIcon,
  User,
  Waves,
} from "lucide-react"
import { motion, useReducedMotion } from "motion/react"

import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  DynamicContainer,
  DynamicDescription,
  DynamicDiv,
  DynamicIsland,
  DynamicIslandProvider,
  DynamicTitle,
  SizePresets,
  useDynamicIslandSize,
  useScheduledAnimations,
} from "@/components/ui/dynamic-island"

const DynamicAction = () => {
  const { state: blobState, setSize } = useDynamicIslandSize()

  const blobStates: SizePresets[] = [
    "compact",
    "large",
    "tall",
    "long",
    "medium",
  ]

  const cycleBlobStates = () => {
    const currentIndex = blobStates.indexOf(blobState.size)
    const nextIndex = (currentIndex + 1) % blobStates.length
    setSize(blobStates[nextIndex])
  }

  useScheduledAnimations([
    { size: "compact", delay: 1000 },
    { size: "large", delay: 1200 },
    { size: "tall", delay: 1600 },
    { size: "long", delay: 1800 },
    { size: "medium", delay: 2200 },
  ])

  // Provide dynamic detail in such a beautiful small place :)
  const renderCompactState = () => (
    <DynamicContainer className="flex h-full w-full items-center justify-center">
      <div className="relative flex w-full items-center">
        <DynamicDescription className="absolute left-4 my-auto text-lg font-medium tracking-tighter text-white">
          <MessageCircle className="h-5 w-5 fill-cyan-400 text-cyan-400" />
        </DynamicDescription>

        <DynamicDescription className="absolute right-4 my-auto text-lg font-bold tracking-tighter text-white">
          newcult.co
        </DynamicDescription>
      </div>
    </DynamicContainer>
  )

  // Great for call to action, popping up in users face :)
  const renderLargeState = () => (
    <DynamicContainer className="flex h-full w-full items-center justify-center">
      <div className="relative flex w-full items-center justify-between gap-6 px-4">
        <Loader className="h-12 w-12 animate-spin text-yellow-300" />

        <DynamicTitle className="my-auto text-2xl font-black tracking-tighter text-white">
          loading
        </DynamicTitle>
      </div>
    </DynamicContainer>
  )

  // Great for user onboarding, forms, etc
  const renderTallState = () => (
    <DynamicContainer className="mt-6 flex w-full flex-col items-start gap-1 px-8 font-semibold">
      <DynamicDescription className="rounded-2xl bg-cyan-300 p-2 leading-5 tracking-tight">
        The Cult of Pythagoras
      </DynamicDescription>
      <DynamicDescription className="rounded-2xl bg-cyan-300 p-2 text-left leading-5 tracking-tight">
        Music of the Spheres, an idea that celestial bodies produce a form of
        music through their movements
      </DynamicDescription>

      <DynamicTitle className="text-4xl font-black tracking-tighter text-cyan-100">
        any cool cults?
      </DynamicTitle>
    </DynamicContainer>
  )

  const renderLongState = () => (
    <DynamicContainer className="flex h-full w-full items-center justify-center">
      <DynamicDiv className="relative flex w-full items-center justify-between gap-6 px-4">
        <div>
          <Waves className="h-8 w-8 text-cyan-400" />
        </div>

        <DynamicTitle className="my-auto text-xl font-black tracking-tighter text-white">
          Supercalifragilisticexpialid
        </DynamicTitle>
      </DynamicDiv>
    </DynamicContainer>
  )

  const renderMediumState = () => (
    <DynamicContainer className="flex h-full flex-col justify-between px-2 pt-4 text-left text-white">
      <DynamicTitle className="pl-3 text-2xl font-black tracking-tighter">
        Reincarnation, welcome back
      </DynamicTitle>
      <DynamicDescription className="pl-3 leading-5 text-neutral-500">
        Good for small tasks or call outs
      </DynamicDescription>

      <DynamicDiv className="mt-auto mb-2 flex flex-col space-y-1 rounded-b-2xl bg-neutral-700 p-2">
        <Button>
          <Mail className="mr-2 h-4 w-4 fill-cyan-400 text-neutral-900" /> Login
          with email
        </Button>

        <Button className="mt-1">
          <User className="mr-2 h-4 w-4 fill-cyan-400 text-cyan-400" /> Join the
          cult now
        </Button>
      </DynamicDiv>
    </DynamicContainer>
  )

  // Render function for other states
  const renderOtherStates = () => (
    <div className="flex h-full w-full items-center justify-center">
      <div>
        <ArrowUpLeftSquareIcon className="text-white" />
      </div>
      <p className="text-white">cycle states</p>
    </div>
  )

  // Main render logic based on size
  function renderState() {
    switch (blobState.size) {
      case "compact":
        return renderCompactState()
      case "large":
        return renderLargeState()
      case "tall":
        return renderTallState()
      case "medium":
        return renderMediumState()
      case "long":
        return renderLongState()
      // Optionally add cases for other states as necessary
      default:
        return renderOtherStates()
    }
  }

  return (
    <div className="h-full">
      <div className="flex h-full flex-col gap-4">
        <div className="absolute top-12 left-12">
          {/* {!blobState.isAnimating ? ( */}
          <Button
            onClick={cycleBlobStates}
            disabled={blobState.isAnimating}
            className="mt-4 max-w-[200px] rounded-md border p-2"
          >
            <MousePointerClickIcon className="mr-1 h-4 w-4" />
            Click to cycle states
          </Button>
          {/* ) : null} */}
        </div>
        <div className="absolute top-1 right-2">
          <div>
            <Badge variant="outline">prev - {blobState.previousSize}</Badge>
            <Badge variant="outline">cur -{blobState.size}</Badge>
          </div>
        </div>

        <DynamicIsland id="dynamic-blob">{renderState()}</DynamicIsland>
      </div>
    </div>
  )
}

export default function DynamicIslandDemo() {
  return (
    <DynamicIslandProvider initialSize={"default"}>
      <div>
        <DynamicAction />
      </div>
    </DynamicIslandProvider>
  )
}

const FadeInStaggerContext = createContext(false)

const viewport = { once: true, margin: "0px 0px -200px" }

export function FadeIn(props: any) {
  let shouldReduceMotion = useReducedMotion()
  let isInStaggerGroup = useContext(FadeInStaggerContext)

  return (
    <motion.div
      variants={{
        hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 24 },
        visible: { opacity: 1, y: 0 },
      }}
      transition={{ duration: 0.5 }}
      {...(isInStaggerGroup
        ? {}
        : {
            initial: "hidden",
            whileInView: "visible",
            viewport,
          })}
      {...props}
    />
  )
}

export function FadeInStagger({ faster = false, ...props }) {
  return (
    <FadeInStaggerContext.Provider value={true}>
      <motion.div
        initial="hidden"
        whileInView="visible"
        viewport={viewport}
        transition={{ staggerChildren: faster ? 0.12 : 0.2 }}
        {...props}
      />
    </FadeInStaggerContext.Provider>
  )
}
```

DynamicIsland is a set of composable React primitives for shadcn/ui built with Motion. Use it for notifications, media controls, timers, or status updates that expand and collapse in place.

## References

<Citations>
    <CitationTitle>Inspiration</CitationTitle>
    <CitationList>
      <CitationItem>
        <CitationLink
          href="https://cho.sh/w/9F7F85"
          target="_blank"
          rel="noopener noreferrer"
        >
         Recreating the Dynamic Island by Sunghyun "Siwoo" Cho
        </CitationLink>
      </CitationItem>

    </CitationList>

</Citations>

## Installation

### CLI

```bash
npx shadcn@latest add @cult-ui/dynamic-island
```

### Manual

**Install the following dependencies:**

```bash
npm install motion
```

**Copy and paste the following code into your project.**

```tsx title="dynamic-island.tsx"
"use client"

import React, {
  createContext,
  ReactNode,
  useCallback,
  useContext,
  useEffect,
  useReducer,
  useRef,
  useState,
} from "react"
import { AnimatePresence, motion, useWillChange } from "motion/react"

const stiffness = 400
const damping = 30
const MIN_WIDTH = 691
const MAX_HEIGHT_MOBILE_ULTRA = 400
const MAX_HEIGHT_MOBILE_MASSIVE = 700

const min = (a: number, b: number) => (a < b ? a : b)

export type SizePresets =
  | "reset"
  | "empty"
  | "default"
  | "compact"
  | "compactLong"
  | "large"
  | "long"
  | "minimalLeading"
  | "minimalTrailing"
  | "compactMedium"
  | "medium"
  | "tall"
  | "ultra"
  | "massive"

const SIZE_PRESETS = {
  RESET: "reset",
  EMPTY: "empty",
  DEFAULT: "default",
  COMPACT: "compact",
  COMPACT_LONG: "compactLong",
  LARGE: "large",
  LONG: "long",
  MINIMAL_LEADING: "minimalLeading",
  MINIMAL_TRAILING: "minimalTrailing",
  COMPACT_MEDIUM: "compactMedium",
  MEDIUM: "medium",
  TALL: "tall",
  ULTRA: "ultra",
  MASSIVE: "massive",
} as const

type Preset = {
  width: number
  height?: number
  aspectRatio: number
  borderRadius: number
}

const DynamicIslandSizePresets: Record<SizePresets, Preset> = {
  [SIZE_PRESETS.RESET]: {
    width: 150,
    aspectRatio: 1,
    borderRadius: 20,
  },
  [SIZE_PRESETS.EMPTY]: {
    width: 0,
    aspectRatio: 0,
    borderRadius: 0,
  },
  [SIZE_PRESETS.DEFAULT]: {
    width: 150,
    aspectRatio: 44 / 150,
    borderRadius: 46,
  },
  [SIZE_PRESETS.MINIMAL_LEADING]: {
    width: 52.33,
    aspectRatio: 44 / 52.33,
    borderRadius: 22,
  },
  [SIZE_PRESETS.MINIMAL_TRAILING]: {
    width: 52.33,
    aspectRatio: 44 / 52.33,
    borderRadius: 22,
  },
  [SIZE_PRESETS.COMPACT]: {
    width: 235,
    aspectRatio: 44 / 235,
    borderRadius: 46,
  },
  [SIZE_PRESETS.COMPACT_LONG]: {
    width: 300,
    aspectRatio: 44 / 235,
    borderRadius: 46,
  },
  [SIZE_PRESETS.COMPACT_MEDIUM]: {
    width: 351,
    aspectRatio: 64 / 371,
    borderRadius: 44,
  },
  [SIZE_PRESETS.LONG]: {
    width: 371,
    aspectRatio: 84 / 371,
    borderRadius: 42,
  },
  [SIZE_PRESETS.MEDIUM]: {
    width: 371,
    aspectRatio: 210 / 371,
    borderRadius: 22,
  },
  [SIZE_PRESETS.LARGE]: {
    width: 371,
    aspectRatio: 84 / 371,
    borderRadius: 42,
  },
  [SIZE_PRESETS.TALL]: {
    width: 371,
    aspectRatio: 210 / 371,
    borderRadius: 42,
  },
  [SIZE_PRESETS.ULTRA]: {
    width: 630,
    aspectRatio: 630 / 800,
    borderRadius: 42,
  },
  [SIZE_PRESETS.MASSIVE]: {
    width: 891,
    height: 1900,
    aspectRatio: 891 / 891,
    borderRadius: 42,
  },
}

type BlobStateType = {
  size: SizePresets
  previousSize: SizePresets | undefined
  animationQueue: Array<{ size: SizePresets; delay: number }>
  isAnimating: boolean
}

type BlobAction =
  | { type: "SET_SIZE"; newSize: SizePresets }
  | { type: "INITIALIZE"; firstState: SizePresets }
  | {
      type: "SCHEDULE_ANIMATION"
      animationSteps: Array<{ size: SizePresets; delay: number }>
    }
  | { type: "ANIMATION_END" }

type BlobContextType = {
  state: BlobStateType
  dispatch: React.Dispatch<BlobAction>
  setSize: (size: SizePresets) => void
  scheduleAnimation: (
    animationSteps: Array<{ size: SizePresets; delay: number }>
  ) => void
  presets: Record<SizePresets, Preset>
}

const BlobContext = createContext<BlobContextType | undefined>(undefined)

const blobReducer = (
  state: BlobStateType,
  action: BlobAction
): BlobStateType => {
  switch (action.type) {
    case "SET_SIZE":
      return {
        ...state,
        size: action.newSize,
        previousSize: state.size,
        isAnimating: false, // Only set isAnimating to true if there are more steps
      }
    case "SCHEDULE_ANIMATION":
      return {
        ...state,
        animationQueue: action.animationSteps,
        isAnimating: action.animationSteps.length > 0,
      }
    case "INITIALIZE":
      return {
        ...state,
        size: action.firstState,
        previousSize: SIZE_PRESETS.EMPTY,
        isAnimating: false,
      }
    case "ANIMATION_END":
      return {
        ...state,
        isAnimating: false,
      }
    default:
      return state
  }
}

interface DynamicIslandProviderProps {
  children: React.ReactNode
  initialSize?: SizePresets
  initialAnimation?: Array<{ size: SizePresets; delay: number }>
}

const DynamicIslandProvider: React.FC<DynamicIslandProviderProps> = ({
  children,
  initialSize = SIZE_PRESETS.DEFAULT,
  initialAnimation = [],
}) => {
  const initialState: BlobStateType = {
    size: initialSize,
    previousSize: SIZE_PRESETS.EMPTY,
    animationQueue: initialAnimation,
    isAnimating: initialAnimation.length > 0,
  }

  const [state, dispatch] = useReducer(blobReducer, initialState)

  useEffect(() => {
    const processQueue = async () => {
      for (const step of state.animationQueue) {
        await new Promise((resolve) => setTimeout(resolve, step.delay))
        dispatch({ type: "SET_SIZE", newSize: step.size })
      }
      dispatch({ type: "ANIMATION_END" })
    }

    if (state.animationQueue.length > 0) {
      processQueue()
    }
  }, [state.animationQueue])

  // biome-ignore lint/correctness/useExhaustiveDependencies: needed for dispatch
  const setSize = useCallback(
    (newSize: SizePresets) => {
      if (state.previousSize !== newSize && newSize !== state.size) {
        dispatch({ type: "SET_SIZE", newSize })
      }
    },
    [state.previousSize, state.size, dispatch]
  )

  // biome-ignore lint/correctness/useExhaustiveDependencies: needed for dispatch
  const scheduleAnimation = useCallback(
    (animationSteps: Array<{ size: SizePresets; delay: number }>) => {
      dispatch({ type: "SCHEDULE_ANIMATION", animationSteps })
    },
    [dispatch]
  )

  const contextValue = {
    state,
    dispatch,
    setSize,
    scheduleAnimation,
    presets: DynamicIslandSizePresets,
  }

  return (
    <BlobContext.Provider value={contextValue}>{children}</BlobContext.Provider>
  )
}

const useDynamicIslandSize = () => {
  const context = useContext(BlobContext)
  if (!context) {
    throw new Error(
      "useDynamicIslandSize must be used within a DynamicIslandProvider"
    )
  }
  return context
}

const useScheduledAnimations = (
  animations: Array<{ size: SizePresets; delay: number }>
) => {
  const { scheduleAnimation } = useDynamicIslandSize()
  const animationsRef = useRef(animations)

  useEffect(() => {
    scheduleAnimation(animationsRef.current)
  }, [scheduleAnimation])
}

const DynamicIslandContainer = ({ children }: { children: ReactNode }) => {
  return (
    <div className="z-10 flex h-full w-full items-end justify-center bg-transparent">
      {children}
    </div>
  )
}

const DynamicIsland = ({
  children,
  id,
  ...props
}: {
  children: ReactNode
  id: string
}) => {
  const willChange = useWillChange()
  const [screenSize, setScreenSize] = useState("desktop")

  useEffect(() => {
    const handleResize = () => {
      if (window.innerWidth <= 640) {
        setScreenSize("mobile")
      } else if (window.innerWidth <= 1024) {
        setScreenSize("tablet")
      } else {
        setScreenSize("desktop")
      }
    }

    handleResize()
    window.addEventListener("resize", handleResize)
    return () => window.removeEventListener("resize", handleResize)
  }, [])

  return (
    <DynamicIslandContainer>
      <DynamicIslandContent
        id={id}
        willChange={willChange}
        screenSize={screenSize}
        {...props}
      >
        {children}
      </DynamicIslandContent>
    </DynamicIslandContainer>
  )
}

const calculateDimensions = (
  size: SizePresets,
  screenSize: string,
  currentSize: Preset
): { width: string; height: number } => {
  const isMassiveOnMobile = size === "massive" && screenSize === "mobile"
  const isUltraOnMobile = size === "ultra" && screenSize === "mobile"

  if (isMassiveOnMobile) {
    return { width: "350px", height: MAX_HEIGHT_MOBILE_MASSIVE }
  }

  if (isUltraOnMobile) {
    return { width: "350px", height: MAX_HEIGHT_MOBILE_ULTRA }
  }

  const width = min(currentSize.width, MIN_WIDTH)
  return { width: `${width}px`, height: currentSize.aspectRatio * width }
}

const DynamicIslandContent = ({
  children,
  id,
  willChange,
  screenSize,
  ...props
}: {
  children: React.ReactNode
  id: string
  willChange: any
  screenSize: string
  [key: string]: any
}) => {
  const { state, presets } = useDynamicIslandSize()
  const currentSize = presets[state.size]

  const dimensions = calculateDimensions(state.size, screenSize, currentSize)

  return (
    <motion.div
      id={id}
      className="mx-auto h-0 w-0 items-center justify-center border border-black/10 bg-black text-center text-black transition duration-300 ease-in-out focus-within:bg-neutral-900 hover:shadow-md dark:border dark:border-white/5 dark:focus-within:bg-black"
      animate={{
        width: dimensions.width,
        height: dimensions.height,
        borderRadius: currentSize.borderRadius,
        transition: {
          type: "spring",
          stiffness,
          damping,
        },
        clipPath: `none`,
        transitionEnd: {
          clipPath: `url(#squircle-${state.size})`,
        },
      }}
      style={{ willChange }}
      {...props}
    >
      <AnimatePresence>{children}</AnimatePresence>
    </motion.div>
  )
}

type DynamicContainerProps = {
  className?: string
  children?: React.ReactNode
}

const DynamicContainer = ({ className, children }: DynamicContainerProps) => {
  const willChange = useWillChange()
  const { state } = useDynamicIslandSize()
  const { size, previousSize } = state

  const isSizeChanged = size !== previousSize

  const initialState = {
    opacity: size === previousSize ? 1 : 0,
    scale: size === previousSize ? 1 : 0.9,
    y: size === previousSize ? 0 : 5,
  }

  const animateState = {
    opacity: 1,
    scale: 1,
    y: 0,
  }

  const transition = {
    type: "spring" as const,
    stiffness,
    damping,
    duration: isSizeChanged ? 0.5 : 0.8,
  }

  return (
    <motion.div
      initial={initialState}
      animate={animateState}
      transition={transition}
      exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95, y: 20 }}
      style={{ willChange }}
      className={className}
    >
      {children}
    </motion.div>
  )
}

type DynamicChildrenProps = {
  className?: string
  children?: React.ReactNode
}

const DynamicDiv = ({ className, children }: DynamicChildrenProps) => {
  const { state } = useDynamicIslandSize()
  const { size, previousSize } = state
  const willChange = useWillChange()

  return (
    <motion.div
      initial={{
        opacity: size === previousSize ? 1 : 0,
        scale: size === previousSize ? 1 : 0.9,
      }}
      animate={{
        opacity: size === previousSize ? 0 : 1,
        scale: size === previousSize ? 0.9 : 1,
        transition: {
          type: "spring",
          stiffness,
          damping,
        },
      }}
      exit={{ opacity: 0, filter: "blur(10px)", scale: 0 }}
      style={{ willChange }}
      className={className}
    >
      {children}
    </motion.div>
  )
}

type MotionProps = {
  className: string
  children: React.ReactNode
}

const DynamicTitle = ({ className, children }: MotionProps) => {
  const { state } = useDynamicIslandSize()
  const { size, previousSize } = state
  const willChange = useWillChange()

  return (
    <motion.h3
      className={className}
      initial={{ opacity: 0, scale: 0 }}
      animate={{
        opacity: size === previousSize ? 0 : 1,
        scale: size === previousSize ? 0.9 : 1,
        transition: { type: "spring", stiffness, damping },
      }}
      style={{ willChange }}
    >
      {children}
    </motion.h3>
  )
}

const DynamicDescription = ({ className, children }: MotionProps) => {
  const { state } = useDynamicIslandSize()
  const { size, previousSize } = state
  const willChange = useWillChange()

  return (
    <motion.p
      className={className}
      initial={{ opacity: 0, scale: 0 }}
      animate={{
        opacity: size === previousSize ? 0 : 1,
        scale: size === previousSize ? 0.9 : 1,
        transition: { type: "spring", stiffness, damping },
      }}
      style={{ willChange }}
    >
      {children}
    </motion.p>
  )
}

export {
  DynamicContainer,
  DynamicTitle,
  DynamicDescription,
  DynamicIsland,
  SIZE_PRESETS,
  stiffness,
  DynamicDiv,
  damping,
  DynamicIslandSizePresets,
  BlobContext,
  useDynamicIslandSize,
  useScheduledAnimations,
  DynamicIslandProvider,
}

export default DynamicIsland
```

**Update the import paths to match your project setup.**

## Usage

```tsx
import {
  DynamicIsland,
  DynamicIslandProvider,
  DynamicContainer,
  DynamicDiv,
  DynamicDescription,
  DynamicTitle,
  useDynamicIslandSize,
} from "@/components/ui/dynamic-island";
```

```tsx
const STATES = ["compact", "large", "tall", "long", "medium"] as const;

function DynamicIslandContent() {
  const { state, setSize } = useDynamicIslandSize();

  const cycleState = () => {
    const currentIndex = STATES.indexOf(state.size as (typeof STATES)[number]);
    const nextIndex = (currentIndex + 1) % STATES.length;
    setSize(STATES[nextIndex]);
  };

  const renderState = () => {
    switch (state.size) {
      case "compact":
        return (
          <DynamicContainer className="flex h-full w-full items-center justify-between px-4 text-white">
            <DynamicDescription className="text-sm opacity-80">
              Now playing
            </DynamicDescription>
            <DynamicTitle className="text-sm font-semibold">
              Cult FM
            </DynamicTitle>
          </DynamicContainer>
        );
      case "large":
        return (
          <DynamicContainer className="flex h-full w-full items-center justify-center">
            <DynamicTitle className="text-2xl font-black text-white">
              Syncing...
            </DynamicTitle>
          </DynamicContainer>
        );
      case "tall":
        return (
          <DynamicContainer className="flex h-full w-full flex-col items-start gap-2 p-5 text-white">
            <DynamicTitle className="text-xl font-bold">
              New message
            </DynamicTitle>
            <DynamicDescription className="text-sm text-neutral-300">
              Your team invited you to review the latest release.
            </DynamicDescription>
          </DynamicContainer>
        );
      default:
        return (
          <DynamicContainer className="flex h-full w-full items-center justify-center">
            <DynamicDiv className="text-sm text-white/90">
              Tap to cycle states
            </DynamicDiv>
          </DynamicContainer>
        );
    }
  };

  return (
    <>
      <button onClick={cycleState}>Cycle</button>
      <DynamicIsland id="example-island">{renderState()}</DynamicIsland>
    </>
  );
}

export default function Example() {
  return (
    <DynamicIslandProvider initialSize="default">
      <DynamicIslandContent />
    </DynamicIslandProvider>
  );
}
```

## Anatomy

The Dynamic Island is built from small primitives you compose together:

- `DynamicIslandProvider` manages size state and transition queue.
- `DynamicIsland` renders the animated shell (width, height, radius).
- `DynamicContainer` wraps each state view with enter/exit motion.
- `DynamicTitle`, `DynamicDescription`, and `DynamicDiv` animate content blocks.
- `useDynamicIslandSize` gives you `state`, `setSize`, and `scheduleAnimation`.

## Size Presets

Use these built-in shape presets with `setSize(...)` or `initialSize`.

| Preset            | Intended use            |
| ----------------- | ----------------------- |
| `default`         | Idle base state         |
| `compact`         | Small status indicator  |
| `compactLong`     | Compact but wider row   |
| `compactMedium`   | Medium compact card     |
| `large`           | Prominent status/action |
| `long`            | Horizontal content row  |
| `medium`          | Multi-line panel        |
| `tall`            | Stacked content         |
| `ultra`           | Large immersive panel   |
| `massive`         | Full, oversized layout  |
| `minimalLeading`  | Tiny leading indicator  |
| `minimalTrailing` | Tiny trailing indicator |
| `reset`           | Internal reset shape    |
| `empty`           | Hidden/zero layout      |

## Controlled State Transitions

Switch island layouts directly from app events:

```tsx
const { setSize } = useDynamicIslandSize();

setSize("compact");
setSize("medium");
```

Use the current and previous state to render context-aware UI:

```tsx
const { state } = useDynamicIslandSize()

<span>Current: {state.size}</span>
<span>Previous: {state.previousSize}</span>
```

## Scheduled Animations

Queue a sequence of size transitions for guided flows:

```tsx
import { useScheduledAnimations } from "@/components/ui/dynamic-island";

function IntroSequence() {
  useScheduledAnimations([
    { size: "compact", delay: 800 },
    { size: "large", delay: 1200 },
    { size: "tall", delay: 1400 },
    { size: "medium", delay: 1000 },
  ]);

  return null;
}
```

`delay` is the wait time (in milliseconds) before each step.

## API Reference

### `DynamicIslandProvider`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | Provider contents |
| `initialSize` | `SizePresets` | `"default"` | Initial island size |
| `initialAnimation` | `{ size: SizePresets; delay: number }[]` | `[]` | Optional initial transition queue |

### `DynamicIsland`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | Required id for the animated island element |
| `children` | `ReactNode` | — | State-specific content |
| `...props` | `HTMLMotionProps<"div">` | — | Passed to the root motion container |

### `DynamicContainer`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | — | Additional classes for the animated wrapper |
| `children` | `ReactNode` | — | Content for current size state |

### `DynamicTitle`, `DynamicDescription`, `DynamicDiv`

| Prop        | Type        | Default | Description      |
| ----------- | ----------- | ------- | ---------------- |
| `className` | `string`    | —       | Styling classes  |
| `children`  | `ReactNode` | —       | Animated content |

### `useDynamicIslandSize()`

Returns:

| Key | Type | Description |
| --- | --- | --- |
| `state.size` | `SizePresets` | Current active preset |
| `state.previousSize` | `SizePresets \| undefined` | Previously rendered preset |
| `state.animationQueue` | `{ size: SizePresets; delay: number }[]` | Pending queued transitions |
| `state.isAnimating` | `boolean` | Whether a queued animation sequence is in progress |
| `setSize` | `(size: SizePresets) => void` | Immediately switch to a preset |
| `scheduleAnimation` | `(steps: { size: SizePresets; delay: number }[]) => void` | Queue timed transitions |
| `presets` | `Record<SizePresets, Preset>` | Raw preset dimensions and radii |

## Best Practices

- Keep each size state focused on one job (status, CTA, detail, confirmation).
- Prefer `DynamicContainer` per state to keep transitions smooth and predictable.
- Use `state.isAnimating` to disable controls during scripted animation sequences.
- Keep heavy effects or expensive rendering outside frequently changing state views.
- Place this component in client-rendered UI (`"use client"`), since it depends on browser size and motion.
