{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cut-morph",
  "title": "CutMorph (state changes as film edits)",
  "description": "Opt-in match cuts on the platform's View Transitions: useCutMorph (interruption-safe, exactly-once updates in call order, never cancels transitions it does not own), CutMorphName for one named subject, and withCutMorphMode for React 19.3 <ViewTransition>. Stepped poses on twos, hard image swaps, no crossfades; reduced motion and still are instant cuts.",
  "registryDependencies": [
    "https://frame12.dev/r/frame12-styles.json",
    "https://frame12.dev/r/use-tempo.json"
  ],
  "files": [
    {
      "path": "ui/cut-morph.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { flushSync } from 'react-dom';\nimport { useResolvedMotion } from '@/hooks/use-tempo';\n\n/**\n * CutMorph: state changes as film edits, on the platform's View Transitions.\n *\n * - `match`: a match cut. The named subject keeps its identity from one shot to the next: its\n *   geometry moves on twos (or smooth at 60), its old and new images hard-swap at the geometric\n *   midpoint, never crossfade.\n * - `cut`: the shot changes instantly. No View Transition at all.\n *\n * Opt-in only. Calling `useCutMorph` is the opt-in; nothing in FRAME/12 animates through it by\n * default. The grammar lives in `frame12.css` (`f12-match`, `f12-cut`, `f12-vt-stepped`,\n * `f12-vt-smooth`), so React 19.3's `<ViewTransition>` can use it too (see `withCutMorphMode`).\n */\nexport type CutMorphKind = 'match' | 'cut';\n/** The transition-scoped mode key: `stepped` (12, on twos) or `smooth` (60). */\nexport type CutMorphMode = 'stepped' | 'smooth';\n\nexport interface UseCutMorphOptions {\n  /**\n   * Accepted for compatibility and currently unused: every CutMorph edit is a document\n   * transition with the root excluded. Chromium 153's element-scoped transitions abort\n   * (\"Prepaint layout check failed\") when the update removes or hides a named subject, which is\n   * what filters do, so CutMorph does not start them (docs/architecture/cut-morph.md).\n   */\n  scope?: React.RefObject<HTMLElement | null>;\n}\n\nexport interface CutMorphCallOptions {\n  /** `match` (default) animates the named subjects; `cut` updates immediately. */\n  kind?: CutMorphKind;\n}\n\n/**\n * Runs `update` (synchronous; it must not return a promise) exactly once, in call order. Resolves\n * once the edit is committed: when the View Transition finishes, or, on the immediate paths, after\n * the calling component's next commit. Rejects only with what `update` threw.\n */\nexport type CutMorph = (update: () => void, opts?: CutMorphCallOptions) => Promise<void>;\n\nexport interface CutMorphNameProps {\n  /** The `view-transition-name`: `^[a-z][a-z0-9-]{0,63}$`, from a stable data id (never an array index). */\n  name: string;\n  /**\n   * The `view-transition-class` list, NOT the DOM class (the child's own `className` is kept):\n   * `f12-match` (default) or `f12-cut`; other classes are added to `f12-match`, never instead.\n   */\n  className?: 'f12-match' | string;\n  /** One DOM host element, or a component that forwards `style` and `ref` to one. */\n  children: React.ReactElement;\n}\n\n/** What the current engine supports (all false on the server). */\nexport interface CutMorphSupport {\n  /** Same-document View Transitions with `view-transition-class`: CutMorph animates. */\n  viewTransitions: boolean;\n  /** The engine has `Element.prototype.startViewTransition` (CutMorph does not use it yet). */\n  elementScoped: boolean;\n  /** `document.activeViewTransition`: transitions CutMorph does not own are detected and left alone. */\n  activeViewTransition: boolean;\n}\n\n// Bundlers (Next.js, webpack, Vite) statically replace `process.env.NODE_ENV`. Declared locally so\n// browser-only TypeScript consumers without @types/node still compile (as in panel.tsx).\ndeclare const process: { env: { NODE_ENV?: string } };\n\n// Development checks test `process.env.NODE_ENV` inline and unguarded, so a bundler's production\n// define folds them away with their messages. Safe wherever React from npm runs: React's own\n// entry point reads process.env.NODE_ENV unguarded too.\n\nconst useIsoLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;\n\ninterface VtLike {\n  ready: Promise<unknown>;\n  finished: Promise<unknown>;\n  updateCallbackDone: Promise<unknown>;\n  skipTransition(): void;\n}\ntype VtStarter = { startViewTransition?: (cb: () => void) => VtLike };\n\n/** One cutMorph call's update, and what it threw. */\ninterface CutMorphEntry {\n  update: () => void;\n  error: { value: unknown } | null;\n}\n\ninterface CutMorphRecord {\n  gen: number;\n  root: Element;\n  mode: 'stepped' | 'smooth';\n  /**\n   * The updates this transition commits, in call order. Calls that arrive before its update\n   * callback has run (same root and mode) join it instead of superseding it: one edit to the\n   * final state, and no flushSync outside the browser's callback.\n   */\n  entries: CutMorphEntry[];\n  updateRan: boolean;\n  vt: VtLike | null;\n  /** The hooks whose calls are in this transition (tempo → still in any of them skips it). */\n  hooks: Set<object>;\n  /** Settles (never rejects) when the transition is over and the record is finished. */\n  done: Promise<void>;\n  detach: () => void;\n}\n\ninterface CutMorphOwner {\n  version: 1;\n  gen: number;\n  /** Owned transitions in flight, oldest first. */\n  active: CutMorphRecord[];\n  /** Which generation set the mode class on a transition root (and the observer holding it). */\n  classes: WeakMap<Element, { gen: number; cls: string; observer?: MutationObserver }>;\n  /** True while run() itself calls startViewTransition (withCutMorphMode must not adopt it). */\n  starting?: boolean;\n  /** withCutMorphMode's open window: the mode key the next transition React starts gets. */\n  pending?: { gen: number; cls: string; timer: ReturnType<typeof setTimeout> } | null;\n  /** The document's own-property startViewTransition wrapper while a window is open. */\n  wrapper?: (...args: unknown[]) => VtLike;\n  /** Development only: live CutMorphName registrations by name. */\n  names: Map<string, Array<{ stack: string }>>;\n}\n\nconst OWNER_KEY = Symbol.for('frame12.cutmorph');\nconst MODE_CLASS: Record<CutMorphMode, string> = {\n  stepped: 'f12-vt-stepped',\n  smooth: 'f12-vt-smooth',\n};\nconst REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';\n\n/** One owner per document, shared by every React root and every copied-in copy of this file. */\nfunction getOwner(): CutMorphOwner {\n  const g = globalThis as unknown as Record<symbol, CutMorphOwner | undefined>;\n  let owner = g[OWNER_KEY];\n  if (!owner) {\n    owner = { version: 1, gen: 0, active: [], classes: new WeakMap(), names: new Map() };\n    g[OWNER_KEY] = owner;\n  }\n  // A record created by another copy (or version) of this file may lack a field: fill it in.\n  owner.gen ??= 0;\n  owner.active ??= [];\n  owner.classes ??= new WeakMap();\n  owner.names ??= new Map();\n  return owner;\n}\n\nfunction setModeClass(root: Element, cls: string, gen: number): void {\n  const owner = getOwner();\n  const prev = owner.classes.get(root);\n  if (prev) {\n    prev.observer?.disconnect();\n    if (prev.cls !== cls) root.classList.remove(prev.cls);\n  }\n  root.classList.add(cls);\n  // The root's className may be React-managed (<html> in some frameworks):\n  // if a render rewrites it mid-transition, put the key back before the next frame (observer\n  // callbacks are microtasks, so this lands before the browser captures or paints).\n  let observer: MutationObserver | undefined;\n  if (typeof MutationObserver === 'function') {\n    observer = new MutationObserver(() => {\n      const cur = owner.classes.get(root);\n      if (cur && cur.gen === gen && !root.classList.contains(cls)) root.classList.add(cls);\n    });\n    observer.observe(root, { attributes: true, attributeFilter: ['class'] });\n  }\n  owner.classes.set(root, { gen, cls, observer });\n}\n\n/** Generation-guarded: a stale `finally` never strips a newer generation's class. */\nfunction clearModeClass(root: Element, gen: number): void {\n  const owner = getOwner();\n  const cur = owner.classes.get(root);\n  if (!cur || cur.gen !== gen) return;\n  cur.observer?.disconnect();\n  root.classList.remove(cur.cls);\n  owner.classes.delete(root);\n}\n\nexport function getCutMorphSupport(): CutMorphSupport {\n  // `document` implies `window` and `Element` (not `CSS`: jsdom has no CSS object).\n  if (typeof document === 'undefined') {\n    return { viewTransitions: false, elementScoped: false, activeViewTransition: false };\n  }\n  // CSS.supports(property, value) never throws.\n  const classes = typeof CSS !== 'undefined' && CSS.supports('view-transition-class', 'f12-match');\n  const doc = document as unknown as VtStarter;\n  return {\n    viewTransitions: typeof doc.startViewTransition === 'function' && classes,\n    elementScoped: typeof (Element.prototype as unknown as VtStarter).startViewTransition === 'function',\n    activeViewTransition: 'activeViewTransition' in document,\n  };\n}\n\nfunction callUpdate(update: () => void): void {\n  const result: unknown = update();\n  if (\n    process.env.NODE_ENV !== 'production' &&\n    result != null &&\n    typeof (result as { then?: unknown }).then === 'function'\n  ) {\n    console.warn(\n      '[frame12] useCutMorph: `update` returned a promise. It must be synchronous: CutMorph snapshots the DOM right after it returns. Await your data first, then call the cut.',\n    );\n  }\n}\n\n/**\n * Runs a record's updates once, in call order. Each update's own throw is kept on its entry (the\n * others still run); a render error React rethrows from flushSync fails the whole batch. Returns\n * the first error, if any.\n */\nfunction runEntries(rec: CutMorphRecord, sync: boolean): { value: unknown } | null {\n  rec.updateRan = true;\n  let first: { value: unknown } | null = null;\n  const all = () => {\n    for (const entry of rec.entries) {\n      try {\n        callUpdate(entry.update);\n      } catch (e) {\n        entry.error = { value: e };\n        first ??= entry.error;\n      }\n    }\n  };\n  try {\n    if (sync) flushSync(all);\n    else all();\n  } catch (e) {\n    const error = { value: e };\n    for (const entry of rec.entries) entry.error ??= error;\n    first ??= error;\n  }\n  return first;\n}\n\n/** A call's promise: settles with its transition and rejects only with its own update's throw. */\nfunction outcome(rec: CutMorphRecord, entry: CutMorphEntry): Promise<void> {\n  return rec.done.then(() => {\n    if (entry.error) throw entry.error.value;\n  });\n}\n\n/** Runs every owned update that has not run yet, oldest first, so call order is kept. */\nfunction flushPending(owner: CutMorphOwner, sync: boolean): void {\n  for (const rec of owner.active) {\n    if (!rec.updateRan) runEntries(rec, sync);\n  }\n}\n\nfunction skipOwned(owner: CutMorphOwner, keep: (rec: CutMorphRecord) => boolean): void {\n  for (const rec of owner.active.slice()) {\n    if (keep(rec)) continue;\n    try {\n      rec.vt?.skipTransition();\n    } catch {\n      /* already finished */\n    }\n  }\n}\n\nfunction finish(rec: CutMorphRecord): void {\n  const owner = getOwner();\n  const i = owner.active.indexOf(rec);\n  if (i >= 0) owner.active.splice(i, 1);\n  rec.detach();\n  clearModeClass(rec.root, rec.gen);\n}\n\ntype ActiveVt = { activeViewTransition?: VtLike | null };\n\nfunction run(\n  update: () => void,\n  kind: CutMorphKind,\n  mode: 'stepped' | 'smooth' | 'cut',\n  hook: object,\n  afterCommit: (done: () => void) => void,\n): Promise<void> {\n  const owner = getOwner();\n  const gen = ++owner.gen;\n\n  const immediate = (): Promise<void> => {\n    // Earlier edits first (exactly once, in order), then the latest edit supersedes them.\n    flushPending(owner, false);\n    skipOwned(owner, () => false);\n    try {\n      callUpdate(update);\n    } catch (e) {\n      return Promise.reject(e);\n    }\n    // No flushSync here: the promise resolves once the calling component has committed.\n    return new Promise<void>((resolve) => afterCommit(resolve));\n  };\n\n  const support = getCutMorphSupport();\n  if (\n    kind === 'cut' ||\n    mode === 'cut' ||\n    !support.viewTransitions ||\n    document.visibilityState === 'hidden'\n  ) {\n    return immediate();\n  }\n  // Never cancel a transition CutMorph does not own (React's <ViewTransition>, a router, the app).\n  const owned = (vt: VtLike | null | undefined) => !vt || owner.active.some((rec) => rec.vt === vt);\n  if (!owned((document as unknown as ActiveVt).activeViewTransition)) return immediate();\n\n  const root: Element = document.documentElement;\n\n  // Join the latest owned transition while its update callback has not run yet (same root and\n  // mode, nothing else pending): its callback runs every queued update in call order inside one\n  // flushSync, so no update is flushed from the caller's context (which React refuses inside a\n  // render or an effect) and the viewer sees one edit to the final state.\n  const last = owner.active[owner.active.length - 1];\n  if (\n    last &&\n    !last.updateRan &&\n    last.vt &&\n    last.root === root &&\n    last.mode === mode &&\n    owner.active.every((rec) => rec === last || rec.updateRan)\n  ) {\n    const entry: CutMorphEntry = { update, error: null };\n    last.entries.push(entry);\n    last.hooks.add(hook);\n    return outcome(last, entry);\n  }\n\n  flushPending(owner, true);\n  // A new transition supersedes every owned one.\n  skipOwned(owner, () => false);\n\n  const entry: CutMorphEntry = { update, error: null };\n  let markDone: () => void = () => {};\n  const rec: CutMorphRecord = {\n    gen,\n    root,\n    mode,\n    entries: [entry],\n    updateRan: false,\n    vt: null,\n    hooks: new Set([hook]),\n    done: new Promise<void>((resolve) => {\n      markDone = resolve;\n    }),\n    detach: () => {},\n  };\n  const listeners: Array<() => void> = [];\n  rec.detach = () => {\n    for (const off of listeners.splice(0)) off();\n  };\n\n  setModeClass(root, MODE_CLASS[mode], gen);\n  owner.active.push(rec);\n\n  const onUpdate = () => {\n    if (rec.updateRan) return;\n    // A throw makes the browser skip the transition (the DOM may be half-updated).\n    const error = runEntries(rec, true);\n    if (error) throw error.value;\n  };\n\n  let vt: VtLike;\n  owner.starting = true;\n  try {\n    vt = (document as unknown as Required<VtStarter>).startViewTransition(onUpdate);\n  } catch {\n    finish(rec);\n    markDone();\n    if (rec.updateRan) return outcome(rec, entry);\n    rec.updateRan = true;\n    return immediate();\n  } finally {\n    owner.starting = false;\n  }\n  rec.vt = vt;\n\n  const skip = () => {\n    try {\n      vt.skipTransition();\n    } catch {\n      /* already finished */\n    }\n  };\n  // Reduced motion turning on mid-transition: skip now (the CSS also stops it).\n  if (typeof window.matchMedia === 'function') {\n    const mq = window.matchMedia(REDUCED_MOTION_QUERY);\n    const onChange = () => {\n      if (mq.matches) skip();\n    };\n    // Every engine with View Transitions has MediaQueryList.addEventListener.\n    mq.addEventListener('change', onChange);\n    listeners.push(() => mq.removeEventListener('change', onChange));\n  }\n\n  let settled = false;\n  const settle = () => {\n    if (settled) return;\n    settled = true;\n    finish(rec);\n    markDone();\n  };\n  vt.updateCallbackDone.catch(() => {});\n  vt.ready.then(\n    () => {\n      if (settled) return;\n      // Document snapshots stay where they were captured: a page scroll, resize, pointer press\n      // or scrolling input would detach them from the content, so the edit lands at once (the\n      // DOM is already updated). Scrolls the update itself caused (a focus() inside a nested\n      // scroller, the page clamping to a shorter document) happened before this baseline or\n      // inside an element, and do not skip it.\n      const sx = window.scrollX;\n      const sy = window.scrollY;\n      const onScroll = () => {\n        if (window.scrollX !== sx || window.scrollY !== sy) skip();\n      };\n      const onKey = (e: KeyboardEvent) => {\n        if (e.key === 'PageUp' || e.key === 'PageDown') skip();\n      };\n      const opts = { capture: true, passive: true };\n      const on: Array<[string, (e: never) => void, AddEventListenerOptions | boolean]> = [\n        ['scroll', onScroll, { passive: true }],\n        ['resize', skip, false],\n        ['pointerdown', skip, opts],\n        ['wheel', skip, opts],\n        ['touchmove', skip, opts],\n        ['keydown', onKey, true],\n      ];\n      for (const [type, fn, o] of on) window.addEventListener(type, fn as EventListener, o);\n      listeners.push(() => {\n        for (const [type, fn, o] of on) window.removeEventListener(type, fn as EventListener, o);\n      });\n    },\n    () => {\n      /* skipped (superseded, duplicate name, hidden document): not an error */\n    },\n  );\n  vt.finished.then(settle, settle);\n  // Superseded and skipped transitions resolve; each call rejects only with its own throw.\n  return outcome(rec, entry);\n}\n\n/**\n * Returns `cutMorph(update, { kind })`. Resolution is from THIS component's tempo context\n * (`useResolvedMotion('stepped')`), so a nested `TempoProvider value=\"still\"` stays still:\n *\n * | Request | Effective mode                                       | Result                             |\n * |---------|------------------------------------------------------|------------------------------------|\n * | `cut`   | any                                                  | `update()` now, no View Transition |\n * | `match` | `cut` (reduced motion, still, still ancestor)        | `update()` now                     |\n * | `match` | no API / no `view-transition-class` / SSR / hidden   | `update()` now                     |\n * | `match` | a transition CutMorph does not own is active         | `update()` now, never cancels it   |\n * | `match` | `stepped`                                            | View Transition, 3 poses on twos   |\n * | `match` | `smooth`                                             | View Transition, 260ms smooth      |\n *\n * Calls that arrive before the running edit's update has been applied (same root and mode: two\n * quick clicks in one frame, or two calls from one effect) join that edit, so the viewer sees one\n * cut to the final state; later calls supersede it. Either way every update runs exactly once, in\n * call order. Call it from discrete actions (event handlers, a subscription callback where data\n * arrives), not from an effect reacting to state. Never wrap pointer drag, typing, focus\n * movement, playback or continuous scrubbing.\n */\nexport function useCutMorph(opts?: UseCutMorphOptions): CutMorph {\n  const mode = useResolvedMotion('stepped');\n  const modeRef = React.useRef(mode);\n  const [hook] = React.useState(() => ({}));\n  // Immediate paths resolve after this component's next commit (the update is on screen then).\n  const waiters = React.useRef<Array<() => void>>([]);\n  const mounted = React.useRef(false);\n  const [, bump] = React.useReducer((n: number) => n + 1, 0);\n  const drain = () => {\n    for (const done of waiters.current.splice(0)) done();\n  };\n  useIsoLayoutEffect(() => {\n    modeRef.current = mode;\n    drain();\n    // Tempo became still (or reduced motion turned on) here: this component's edits land now.\n    if (mode === 'cut') {\n      const owner = getOwner();\n      skipOwned(owner, (rec) => !rec.hooks.has(hook));\n    }\n  });\n  useIsoLayoutEffect(() => {\n    mounted.current = true;\n    return () => {\n      mounted.current = false;\n      drain();\n    };\n  }, []);\n  return React.useCallback<CutMorph>(\n    (update, callOpts) =>\n      run(\n        update,\n        callOpts?.kind ?? 'match',\n        modeRef.current,\n        hook,\n        (done) => {\n          // Not mounted (the server, or after unmount): nothing will commit here, resolve now.\n          if (!mounted.current) return done();\n          waiters.current.push(done);\n          bump();\n        },\n      ),\n    [hook],\n  );\n}\n\n/**\n * For React 19.3 `<ViewTransition default=\"f12-match\">` users: the next View Transition started\n * after this call (the one React starts for `fn`'s `startTransition`, within ~1s) gets the\n * CutMorph mode key on `<html>` from the moment it starts until it finishes. Nothing is restyled\n * before it starts, and only that one transition is adopted. Pass the mode from\n * `useResolvedMotion`; when it resolves to `cut`, update outside a Transition instead.\n */\nexport function withCutMorphMode<T>(mode: 'stepped' | 'smooth', fn: () => T): T {\n  if (typeof document === 'undefined' || !MODE_CLASS[mode]) return fn();\n  const owner = getOwner();\n  const doc = document as unknown as { startViewTransition?: (...a: unknown[]) => VtLike };\n  if (typeof doc.startViewTransition !== 'function') return fn();\n  if (owner.pending) clearTimeout(owner.pending.timer);\n  const gen = ++owner.gen;\n  // One own-property wrapper on this document while a window is open; removed when it is used\n  // or when the window closes unused.\n  const unwrap = () => {\n    if (owner.wrapper && doc.startViewTransition === owner.wrapper) delete doc.startViewTransition;\n    owner.wrapper = undefined;\n  };\n  const close = () => {\n    if (owner.pending?.gen !== gen) return;\n    owner.pending = null;\n    unwrap();\n  };\n  owner.pending = { gen, cls: MODE_CLASS[mode], timer: setTimeout(close, 1000) };\n  if (!owner.wrapper || doc.startViewTransition !== owner.wrapper) {\n    const original = doc.startViewTransition;\n    const wrapper = (...args: unknown[]): VtLike => {\n      if (owner.starting) return original.apply(document, args);\n      const win = owner.pending;\n      owner.pending = null;\n      unwrap();\n      if (!win) return original.apply(document, args);\n      clearTimeout(win.timer);\n      const root = document.documentElement;\n      setModeClass(root, win.cls, win.gen);\n      let vt: VtLike;\n      try {\n        vt = original.apply(document, args);\n      } catch (e) {\n        clearModeClass(root, win.gen);\n        throw e;\n      }\n      const done = () => clearModeClass(root, win.gen);\n      vt.finished.then(done, done);\n      return vt;\n    };\n    owner.wrapper = wrapper;\n    doc.startViewTransition = wrapper;\n  }\n  try {\n    return fn();\n  } catch (e) {\n    close();\n    throw e;\n  }\n}\n\n// ------------------------------------------------------------------------------------- names\n\nconst NAME_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;\n// Not valid view-transition names: the property's own keywords, the root's UA name, the\n// CSS-wide keywords and `default` (reserved for every <custom-ident>).\nconst RESERVED_NAMES = new Set([\n  'none',\n  'auto',\n  'match-element',\n  'root',\n  'initial',\n  'inherit',\n  'unset',\n  'revert',\n  'revert-layer',\n  'default',\n]);\n\n/** True for a name `CutMorphName` accepts. */\nexport function isCutMorphName(name: unknown): name is string {\n  return typeof name === 'string' && NAME_PATTERN.test(name) && !RESERVED_NAMES.has(name);\n}\n\nfunction classList(value: string): string | null {\n  const parts = value.trim().split(/\\s+/);\n  for (const p of parts) {\n    if (!/^-?[a-z_][a-z0-9_-]*$/i.test(p) || p === 'none' || RESERVED_NAMES.has(p)) return null;\n  }\n  // Your own classes are added to the grammar, never instead of it (without f12-match or\n  // f12-cut the browser's default crossfade would play).\n  if (!parts.includes('f12-match') && !parts.includes('f12-cut')) parts.unshift('f12-match');\n  return parts.join(' ');\n}\n\nfunction captureStack(): string {\n  const r = React as unknown as { captureOwnerStack?: () => string | null };\n  try {\n    const owner = typeof r.captureOwnerStack === 'function' ? r.captureOwnerStack() : null;\n    if (owner) return owner;\n  } catch {\n    /* not available in this build */\n  }\n  return (new Error('CutMorphName').stack || '').split('\\n').slice(2, 8).join('\\n');\n}\n\n/**\n * Names one subject for match cuts: sets `view-transition-name` (and `view-transition-class`,\n * `f12-match` by default) on its single DOM host child, merging the child's own `style`,\n * `className`, `ref` and handlers. The name is removed on unmount. Invalid names and children\n * that cannot carry a style (Fragment, text, arrays) are a development error and render\n * unchanged in production.\n */\nexport function CutMorphName({\n  name,\n  className = 'f12-match',\n  children,\n}: CutMorphNameProps): React.ReactElement {\n  const isHost =\n    React.isValidElement(children) && (children as React.ReactElement).type !== React.Fragment;\n  const validName = isCutMorphName(name);\n  const vtClass = typeof className === 'string' ? classList(className) : null;\n  const usable = isHost && validName && vtClass !== null;\n\n  const stackRef = React.useRef('');\n  if (process.env.NODE_ENV !== 'production' && !stackRef.current) stackRef.current = captureStack();\n  const registered = usable ? name : null;\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === 'production' || !registered) return;\n    const owner = getOwner();\n    const list = owner.names.get(registered) ?? [];\n    const entry = { stack: stackRef.current };\n    if (list.length > 0) {\n      console.warn(\n        `[frame12] CutMorphName: \"${registered}\" is already on screen. view-transition-name must be unique among live elements; with a duplicate the browser skips the transition (the update still lands). Use a stable data id.\\n--- first owner ---\\n${list[0].stack}\\n--- second owner ---\\n${entry.stack}`,\n      );\n    }\n    list.push(entry);\n    owner.names.set(registered, list);\n    return () => {\n      const cur = owner.names.get(registered);\n      if (!cur) return;\n      const i = cur.indexOf(entry);\n      if (i >= 0) cur.splice(i, 1);\n      if (cur.length === 0) owner.names.delete(registered);\n    };\n  }, [registered]);\n\n  if (!usable) {\n    if (process.env.NODE_ENV !== 'production') {\n      if (!isHost) {\n        console.error(\n          '[frame12] CutMorphName needs one DOM host element child (or a component that forwards `style` and `ref`), not a Fragment, text or an array.',\n        );\n      } else if (!validName) {\n        console.error(\n          `[frame12] CutMorphName: invalid name ${JSON.stringify(name)}. Use ^[a-z][a-z0-9-]{0,63}$ from a stable data id; none, auto, match-element, root, default and the CSS-wide keywords are reserved.`,\n        );\n      } else {\n        console.error(\n          `[frame12] CutMorphName: invalid className ${JSON.stringify(className)} (a view-transition-class list).`,\n        );\n      }\n    }\n    return isHost ? (children as React.ReactElement) : <>{children}</>;\n  }\n  const child = children as React.ReactElement<{ style?: React.CSSProperties }>;\n  const style = {\n    ...(child.props.style || {}),\n    viewTransitionName: name,\n    viewTransitionClass: vtClass,\n  } as React.CSSProperties;\n  return React.cloneElement(child, { style });\n}\n",
      "type": "registry:ui",
      "target": "components/frame12/cut-morph.tsx"
    }
  ],
  "type": "registry:ui"
}