{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dynamic-island",
  "type": "registry:ui",
  "title": "Dynamic Island",
  "description": "An Apple-style Dynamic Island that morphs between idle, ring, silent, and timer states with spring physics and blurred content cross-fades using Motion.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/crafterui/ui/dynamic-island.tsx",
      "type": "registry:ui",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  AnimatePresence,\n  MotionConfig,\n  motion,\n  type Transition,\n} from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/** The visual state the Dynamic Island can morph between. */\nexport type IslandState = \"idle\" | \"ring\" | \"silent\" | \"timer\"\n\n/** Every state, in display order - handy for building your own controls. */\nexport const ISLAND_STATES: readonly IslandState[] = [\n  \"idle\",\n  \"ring\",\n  \"silent\",\n  \"timer\",\n]\n\nexport interface DynamicIslandProps {\n  /**\n   * Drive the state from outside (controlled). Leave undefined to let the\n   * island own its state and start from `defaultState`. @default undefined\n   */\n  state?: IslandState\n  /** State to start in when uncontrolled. @default \"silent\" */\n  defaultState?: IslandState\n  /** Extra classes for the outer wrapper. @default undefined */\n  className?: string\n  /** Called whenever the state changes, from either the caller or the timer. @default undefined */\n  onStateChange?: (state: IslandState) => void\n  /**\n   * Total seconds the timer state counts down from.\n   * @default 30\n   */\n  timerSeconds?: number\n}\n\n/** Spring used for the island morph (size + radius) and the toggle thumb slide. */\nconst ISLAND_SPRING: Transition = {\n  type: \"spring\",\n  stiffness: 380,\n  damping: 30,\n  mass: 0.9,\n}\n\n/** Tween for the content cross-fade. */\nconst CONTENT_TWEEN: Transition = {\n  duration: 0.22,\n  ease: \"easeOut\",\n}\n\n/** Per-state geometry of the pill. */\nconst GEOMETRY: Record<\n  IslandState,\n  { width: number; height: number; radius: number }\n> = {\n  idle: { width: 96, height: 28, radius: 32 },\n  ring: { width: 148, height: 28, radius: 32 },\n  silent: { width: 148, height: 28, radius: 32 },\n  timer: { width: 264, height: 52, radius: 26 },\n}\n\n/**\n * Apple's system accents for each state. Exported so callers can colour their\n * own controls to match - these are the replica's fixed palette, not theme\n * tokens, because the island itself is always the device's black pill.\n */\nexport const ISLAND_ACCENTS: Partial<Record<IslandState, string>> = {\n  ring: \"#30D158\",\n  silent: \"#FD4F30\",\n  timer: \"#E9A23C\",\n}\n\nconst SILENT_RED = ISLAND_ACCENTS.silent!\nconst RING_GREEN = ISLAND_ACCENTS.ring!\nconst TIMER_AMBER = ISLAND_ACCENTS.timer!\n\n/** Cross-fade variants: blur + scale in/out, Apple-style. */\nconst fade = {\n  initial: { opacity: 0, filter: \"blur(4px)\", scale: 0.9 },\n  animate: { opacity: 1, filter: \"blur(0px)\", scale: 1 },\n  exit: { opacity: 0, filter: \"blur(4px)\", scale: 0.9 },\n}\n\nfunction BellIcon() {\n  return (\n    <svg\n      className=\"absolute inset-0\"\n      width=\"11.25\"\n      height=\"12.75\"\n      viewBox=\"0 0 15 17\"\n      fill=\"none\"\n    >\n      <path\n        d=\"M1.17969 13.3125H13.5625C14.2969 13.3125 14.7422 12.9375 14.7422 12.3672C14.7422 11.5859 13.9453 10.8828 13.2734 10.1875C12.7578 9.64844 12.6172 8.53906 12.5547 7.64062C12.5 4.64062 11.7031 2.57812 9.625 1.82812C9.32812 0.804688 8.52344 0 7.36719 0C6.21875 0 5.40625 0.804688 5.11719 1.82812C3.03906 2.57812 2.24219 4.64062 2.1875 7.64062C2.125 8.53906 1.98438 9.64844 1.46875 10.1875C0.789062 10.8828 0 11.5859 0 12.3672C0 12.9375 0.4375 13.3125 1.17969 13.3125ZM7.36719 16.4453C8.69531 16.4453 9.66406 15.4766 9.76562 14.3828H4.97656C5.07812 15.4766 6.04688 16.4453 7.36719 16.4453Z\"\n        fill=\"white\"\n      />\n    </svg>\n  )\n}\n\n/** The diagonal slash that \"draws\" across the bell to mean silent. */\nfunction BellSlash({ visible }: { visible: boolean }) {\n  return (\n    <div className=\"absolute inset-0\">\n      <div className=\"h-5 translate-x-[5.25px] -translate-y-[5px] rotate-[-40deg]\">\n        <motion.div\n          className=\"w-fit overflow-hidden rounded-full\"\n          initial={false}\n          animate={{ height: visible ? 16 : 0 }}\n          transition={{ type: \"spring\", stiffness: 420, damping: 32 }}\n        >\n          <div className=\"flex h-4 w-[3px] items-center justify-center rounded-full bg-[#FD4F30]\">\n            <div className=\"h-full w-[0.75px] rounded-full bg-white\" />\n          </div>\n        </motion.div>\n      </div>\n    </div>\n  )\n}\n\n/**\n * Compact ring/silent display. A single colored thumb slides between the\n * bell (left) and the label (right), recoloring red<->green, while the bell\n * gains/loses its slash and the label swaps Silent<->Ring.\n */\nfunction CompactContent({ state }: { state: \"ring\" | \"silent\" }) {\n  const silent = state === \"silent\"\n  return (\n    <div className=\"relative h-[28px]\" style={{ width: 148 }}>\n      {/* Sliding thumb: left+red+narrow for Silent, right+green+wide for Ring. */}\n      <motion.div\n        className=\"absolute top-1/2 left-[5px] h-[18px] -translate-y-1/2 rounded-full\"\n        initial={false}\n        animate={{\n          width: silent ? 40 : 52,\n          x: silent ? 0 : 82,\n          backgroundColor: silent ? SILENT_RED : RING_GREEN,\n        }}\n        transition={ISLAND_SPRING}\n      />\n\n      {/* Bell, fixed on the left. Sits on the red thumb when silent. */}\n      <span\n        className=\"absolute top-1/2 left-[15px] block h-[12.75px] w-[11.25px] -translate-y-1/2\"\n        aria-hidden\n      >\n        <BellIcon />\n        <BellSlash visible={silent} />\n      </span>\n\n      {/* Label, fixed on the right. Sits on the green thumb when ringing. */}\n      <div className=\"absolute top-1/2 right-[18px] flex -translate-y-1/2 justify-end\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {silent ? (\n            <motion.span\n              key=\"silent\"\n              className=\"block text-xs font-semibold whitespace-nowrap text-[#FD4F30]\"\n              variants={fade}\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              transition={CONTENT_TWEEN}\n            >\n              Silent\n            </motion.span>\n          ) : (\n            <motion.span\n              key=\"ring\"\n              className=\"block text-xs font-semibold whitespace-nowrap text-white\"\n              variants={fade}\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              transition={CONTENT_TWEEN}\n            >\n              Ring\n            </motion.span>\n          )}\n        </AnimatePresence>\n      </div>\n    </div>\n  )\n}\n\nfunction PauseGlyph() {\n  return (\n    <svg width=\"10\" height=\"12\" viewBox=\"0 0 10 12\" fill=\"white\">\n      <rect x=\"0\" width=\"3\" height=\"12\" rx=\"1.2\" />\n      <rect x=\"7\" width=\"3\" height=\"12\" rx=\"1.2\" />\n    </svg>\n  )\n}\n\nfunction PlayGlyph() {\n  return (\n    <svg width=\"11\" height=\"12\" viewBox=\"0 0 11 12\" fill=\"white\">\n      <path d=\"M1 1L10 6L1 11Z\" />\n    </svg>\n  )\n}\n\nfunction CloseGlyph() {\n  return (\n    <svg\n      width=\"11\"\n      height=\"11\"\n      viewBox=\"0 0 11 11\"\n      fill=\"none\"\n      stroke=\"white\"\n      strokeWidth=\"1.8\"\n      strokeLinecap=\"round\"\n    >\n      <path d=\"M1 1L10 10M10 1L1 10\" />\n    </svg>\n  )\n}\n\n/** Timer content: amber pause + gray cancel controls and an amber countdown. */\nfunction TimerContent({\n  seconds,\n  onCancel,\n}: {\n  seconds: number\n  onCancel: () => void\n}) {\n  const [remaining, setRemaining] = React.useState(seconds)\n  const [paused, setPaused] = React.useState(false)\n\n  React.useEffect(() => {\n    setRemaining(seconds)\n    setPaused(false)\n  }, [seconds])\n\n  React.useEffect(() => {\n    if (paused) return\n    const id = window.setInterval(() => {\n      setRemaining((r) => (r <= 0 ? 0 : r - 1))\n    }, 1000)\n    return () => window.clearInterval(id)\n  }, [paused])\n\n  const mm = Math.floor(remaining / 60)\n  const ss = remaining % 60\n\n  return (\n    <div\n      className=\"flex h-[52px] items-center gap-2.5 pr-5 pl-2.5\"\n      style={{ width: 264 }}\n    >\n      <button\n        type=\"button\"\n        onClick={() => setPaused((p) => !p)}\n        aria-label={paused ? \"Resume timer\" : \"Pause timer\"}\n        // White ring, not the theme's: these sit on the pill's fixed black, where\n        // outline-foreground would be near-black on black under the light theme.\n        className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition-transform outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white active:scale-95\"\n        style={{ backgroundColor: TIMER_AMBER }}\n      >\n        {paused ? <PlayGlyph /> : <PauseGlyph />}\n      </button>\n      <button\n        type=\"button\"\n        onClick={onCancel}\n        aria-label=\"Cancel timer\"\n        className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#5A5A5E] transition-transform outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white active:scale-95\"\n      >\n        <CloseGlyph />\n      </button>\n      <div className=\"ml-auto flex items-baseline gap-1.5\">\n        <span className=\"text-sm font-semibold\" style={{ color: TIMER_AMBER }}>\n          Timer\n        </span>\n        <span\n          className=\"text-2xl font-semibold tabular-nums\"\n          style={{ color: TIMER_AMBER }}\n        >\n          {mm}:{ss.toString().padStart(2, \"0\")}\n        </span>\n      </div>\n    </div>\n  )\n}\n\n/**\n * An Apple \"Dynamic Island\": a pure-black pill that morphs its size,\n * radius and content between idle / ring / silent / timer states with\n * spring physics and blurred content cross-fades.\n *\n * Works uncontrolled (`defaultState`) or controlled (`state` + `onStateChange`).\n * It renders only the pill - drive it from your own UI, or copy the control\n * strip out of the demo.\n */\nexport function DynamicIsland({\n  state: controlledState,\n  defaultState = \"silent\",\n  className,\n  onStateChange,\n  timerSeconds = 30,\n}: DynamicIslandProps) {\n  const [uncontrolled, setUncontrolled] =\n    React.useState<IslandState>(defaultState)\n  const state = controlledState ?? uncontrolled\n\n  const setState = React.useCallback(\n    (next: IslandState) => {\n      if (controlledState === undefined) setUncontrolled(next)\n      onStateChange?.(next)\n    },\n    [controlledState, onStateChange]\n  )\n\n  const geo = GEOMETRY[state]\n\n  return (\n    // One provider rather than a reduced-motion branch on each of the eight\n    // transitions below: `user` keeps the opacity cross-fades and drops the\n    // spring-driven size and radius morph, which is the part that moves.\n    <MotionConfig reducedMotion=\"user\">\n    <div className={cn(\"relative w-fit\", className)}>\n      <motion.div\n        className=\"w-fit overflow-hidden bg-black\"\n        initial={false}\n        animate={{\n          width: geo.width,\n          height: geo.height,\n          borderRadius: geo.radius,\n        }}\n        transition={ISLAND_SPRING}\n      >\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {state === \"idle\" && (\n            <motion.div\n              key=\"idle\"\n              variants={fade}\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              transition={CONTENT_TWEEN}\n              className=\"h-[28px] w-[96px]\"\n            />\n          )}\n          {state === \"timer\" && (\n            <motion.div\n              key=\"timer\"\n              variants={fade}\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              transition={CONTENT_TWEEN}\n            >\n              <TimerContent\n                seconds={timerSeconds}\n                onCancel={() => setState(\"idle\")}\n              />\n            </motion.div>\n          )}\n          {(state === \"ring\" || state === \"silent\") && (\n            <motion.div\n              key=\"compact\"\n              variants={fade}\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              transition={CONTENT_TWEEN}\n            >\n              <CompactContent state={state} />\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n    </MotionConfig>\n  )\n}\n"
    }
  ]
}
