{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "icon-transition",
  "title": "Icon Transition",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/icon-transition/IconTransition.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Component } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type { TransitionConfig } from 'svelte/transition'\n\n  // Self-contained icon-swap control. Click runs an optional async `action`,\n  // then flips from `defaultIcon` to `activeIcon` with a spring pop. By default\n  // it auto-reverts after 1500ms; pass `resetAfter={0}` to keep the active\n  // icon, then call `ref.reset()` manually.\n  //\n  // Two operating modes:\n  //   1. Self-managed (most common): pass `defaultIcon`, `activeIcon`, and\n  //      either `action` or just listen to `onActivate`. The component runs\n  //      the action, manages its own `active` state, and auto-resets.\n  //   2. Externally controlled: pass `active={boolean}` and the component\n  //      will mirror that prop, ignoring its internal flag.\n  //\n  // Renders as a <button> by default. Pass `as=\"span\"` for a non-interactive\n  // icon (e.g. inside a parent button) — in that mode the parent is responsible\n  // for triggering `trigger()` via the component ref.\n  /** Imperative handle — grab it with `bind:this` (React `IconTransitionHandle` parity). */\n  export interface IconTransitionHandle {\n    trigger: () => Promise<void>\n    reset: () => void\n  }\n\n  export interface IconTransitionProps extends HTMLAttributes<HTMLButtonElement> {\n    defaultIcon: Component\n    activeIcon: Component\n    /** Tailwind size/color applied to the icons. */\n    iconClass?: string\n    /** Async work executed on click before the icon flips. Return `false` to skip the flip. */\n    action?: () => boolean | void | Promise<boolean | void>\n    /** ms before reverting to defaultIcon. Set to `0` (or null) to stay active. */\n    resetAfter?: number | null\n    /** Pop animation duration in ms (also scales the leave fade). */\n    duration?: number\n    /** aria-label shown in default state. */\n    label?: string\n    /** aria-label shown after activation; falls back to `label`. */\n    activeLabel?: string\n    /** Tailwind class applied while active (e.g. \"text-success\"). */\n    activeClass?: string\n    /** Render element. `button` adds click handler + focus styling; `span` is purely visual. */\n    as?: 'button' | 'span'\n    /** External control. When provided, this prop wins over internal state. */\n    active?: boolean\n    onActivate?: () => void\n    onReset?: () => void\n  }\n\n  // Leave fade matching the Vue `icon-transition-fade` keyframes (opacity\n  // 1→0, scale 1→0.85). Enter is a plain CSS animation (see the style block\n  // below) that autoplays when the keyed icon mounts — Svelte has no\n  // <Transition> wrapper. NOTE: do not spell out a style tag literally in\n  // this comment — svelte-check misparses it as the style block start.\n  function leaveFade(_node: Element, { duration }: { duration: number }): TransitionConfig {\n    if (\n      typeof window !== 'undefined' &&\n      typeof window.matchMedia === 'function' &&\n      window.matchMedia('(prefers-reduced-motion: reduce)').matches\n    ) {\n      return { duration: 0 }\n    }\n    return {\n      duration,\n      easing: (t) => t * t,\n      css: (t) => `opacity: ${t}; transform: scale(${0.85 + 0.15 * t});`,\n    }\n  }\n</script>\n\n<script lang=\"ts\">\n  import { onDestroy } from 'svelte'\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    defaultIcon,\n    activeIcon,\n    iconClass = 'size-4',\n    action,\n    resetAfter = 1500,\n    duration = 240,\n    label,\n    activeLabel,\n    activeClass = 'text-success',\n    as = 'button',\n    active,\n    onActivate,\n    onReset,\n    ...restProps\n  }: IconTransitionProps = $props()\n\n  let internalActive = $state(false)\n  const isActive = $derived(active !== undefined ? active : internalActive)\n\n  let timer: ReturnType<typeof setTimeout> | null = null\n\n  function clearTimer() {\n    if (timer) {\n      clearTimeout(timer)\n      timer = null\n    }\n  }\n\n  export async function trigger() {\n    if (action) {\n      const result = await action()\n      if (result === false) return\n    }\n    internalActive = true\n    onActivate?.()\n    clearTimer()\n    if (resetAfter && resetAfter > 0) {\n      timer = setTimeout(reset, resetAfter)\n    }\n  }\n\n  export function reset() {\n    internalActive = false\n    onReset?.()\n    clearTimer()\n  }\n\n  onDestroy(clearTimer)\n\n  const CurrentIcon = $derived(isActive ? activeIcon : defaultIcon)\n</script>\n\n<svelte:element\n  this={as}\n  data-uipkge\n  data-slot=\"icon-transition\"\n  {...restProps}\n  type={as === 'button' ? 'button' : undefined}\n  aria-label={isActive ? (activeLabel ?? label) : label}\n  aria-live={isActive ? 'polite' : undefined}\n  class={cn(\n    'icon-transition focus-visible:ring-ring inline-grid place-items-center transition-colors focus-visible:ring-2 focus-visible:outline-none',\n    isActive ? activeClass : '',\n    className,\n  )}\n  style=\"--it-duration: {duration}ms\"\n  onclick={as === 'button' ? trigger : undefined}\n>\n  {#key isActive}\n    <!-- Wrapping span isolates the positioning + transform from the icon\n      itself. Putting the transition transform directly on the SVG would\n      stretch it to fill the parent (e.g. 28×28 inside a size-7 button) and\n      the icon would visually balloon mid-transition; the wrapper takes the\n      layout role and the SVG keeps its intrinsic iconClass-driven size. -->\n    <span class=\"icon-transition-slot\" out:leaveFade={{ duration: duration * 0.66 }}>\n      <CurrentIcon class={iconClass} aria-hidden=\"true\" />\n    </span>\n  {/key}\n</svelte:element>\n\n<style>\n  .icon-transition-slot {\n    /* Both default and active icons map to the same grid cell so they can\n     overlap during the cross-fade without ever leaving the parent's flow. */\n    grid-area: 1 / 1;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    transform-origin: center;\n    /* Fill `backwards` (not `both`): the fill must release once the enter\n     animation ends, otherwise it overrides the leaveFade transition's\n     inline opacity/transform during the outro. */\n    animation: icon-transition-pop var(--it-duration, 240ms) cubic-bezier(0.34, 1.56, 0.64, 1) backwards;\n  }\n  @keyframes icon-transition-pop {\n    0% {\n      opacity: 0;\n      transform: scale(0.5) rotate(-12deg);\n    }\n    60% {\n      opacity: 1;\n      transform: scale(1.18) rotate(2deg);\n    }\n    100% {\n      opacity: 1;\n      transform: scale(1) rotate(0deg);\n    }\n  }\n  @media (prefers-reduced-motion: reduce) {\n    .icon-transition-slot {\n      animation-duration: 0ms !important;\n    }\n  }\n</style>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/icon-transition/IconTransition.svelte"
    },
    {
      "path": "packages/registry-svelte/components/icon-transition/index.ts",
      "content": "export {\n  default as IconTransition,\n  type IconTransitionHandle,\n  type IconTransitionProps,\n} from './IconTransition.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/icon-transition/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Animated icon swap for click acknowledgements — copy → check, bookmark → bookmark-filled, follow → follower. Pass two icon components and an async `action`; clicking runs the action then springs the active icon in. Auto-reverts after `resetAfter` ms (default 1500), or pass `0` to keep the active icon and call the exposed `reset()` method via bind:this to flip it back. Renders as a `<button>` by default; use `as=\"span\"` plus the `active` prop when you want an externally controlled, non-interactive icon swap inside another control. Honors `prefers-reduced-motion`.",
  "categories": [
    "feedback"
  ]
}