{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "activity",
  "title": "Activity",
  "description": "A live surface for anything that takes time or keeps going. Compact when you glance, expanded when you look, with elements that keep their identity across the morph.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://kinematic.design/r/motion.json",
    "https://kinematic.design/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "src/registry/kinematic/ui/activity.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, MotionConfig } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { activityStore, type ActivityEntry } from \"@/registry/kinematic/lib/activity-store\"\nimport {\n  Activity,\n  ActivityAnchorContext,\n  type ActivityAnchor,\n  type ActivityPresentation,\n} from \"@/registry/kinematic/ui/activity-surface\"\nimport { ActivityTaskView, taskPhase } from \"@/registry/kinematic/ui/activity-task\"\n\nconst MAX_BUBBLES = 3\nconst NO_ENTRIES: ActivityEntry[] = []\n\nconst POSITIONS = {\n  \"bottom-right\": \"right-4 bottom-4 flex-col items-end\",\n  \"bottom-center\": \"bottom-4 left-1/2 -translate-x-1/2 flex-col items-center\",\n  \"bottom-left\": \"bottom-4 left-4 flex-col items-start\",\n  \"top-right\": \"top-4 right-4 flex-col-reverse items-end\",\n  \"top-center\": \"top-4 left-1/2 -translate-x-1/2 flex-col-reverse items-center\",\n  \"top-left\": \"top-4 left-4 flex-col-reverse items-start\",\n} as const\n\nexport type ActivityPosition = keyof typeof POSITIONS\n\nexport type ActivityLayout = \"stack\" | \"single\" | \"island\"\n\nconst ANCHORS: Record<ActivityPosition, ActivityAnchor> = {\n  \"bottom-right\": { x: \"end\", y: \"end\" },\n  \"bottom-center\": { x: \"center\", y: \"end\" },\n  \"bottom-left\": { x: \"start\", y: \"end\" },\n  \"top-right\": { x: \"end\", y: \"start\" },\n  \"top-center\": { x: \"center\", y: \"start\" },\n  \"top-left\": { x: \"start\", y: \"start\" },\n}\n\nconst JUSTIFY = { start: \"justify-start\", center: \"justify-center\", end: \"justify-end\" } as const\n\n// Room the bubbles take beside a compact primary (54px bubble + 8px gap each), so it truncates instead of clipping.\nconst BUBBLE_RESERVE = [\n  \"\",\n  \"[--activity-reserve:62px]\",\n  \"[--activity-reserve:124px]\",\n  \"[--activity-reserve:186px]\",\n  \"[--activity-reserve:248px]\",\n]\n\nfunction useEntries() {\n  return React.useSyncExternalStore(activityStore.subscribe, activityStore.getEntries, () => NO_ENTRIES)\n}\n\nfunction useFocused() {\n  return React.useSyncExternalStore(activityStore.subscribe, activityStore.getFocused, () => null)\n}\n\ntype EntryProps = {\n  entry: ActivityEntry\n  presentation?: ActivityPresentation\n  className?: string\n}\n\nconst ViewportEntry = React.memo(function ViewportEntry({ entry, presentation = \"full\", className }: EntryProps) {\n  const { id } = entry\n  const setExpanded = React.useCallback((open: boolean) => activityStore.setExpanded(id, open), [id])\n  const dismiss = React.useCallback(() => activityStore.dismiss(id), [id])\n  const focus = React.useCallback(() => activityStore.focus(id), [id])\n\n  return (\n    <Activity\n      id={id}\n      presentation={presentation}\n      expanded={activityStore.isExpanded(entry)}\n      onExpandedChange={setExpanded}\n      onDismiss={dismiss}\n      onFocus={focus}\n      phase={entry.kind === \"task\" ? taskPhase(entry) : undefined}\n      className={className}\n    >\n      {entry.kind === \"task\" ? <ActivityTaskView task={entry} /> : <entry.view />}\n    </Activity>\n  )\n})\n\nfunction MoreBubble({ count, onClick }: { count: number; onClick: () => void }) {\n  return (\n    <motion.button\n      layout\n      type=\"button\"\n      onClick={onClick}\n      aria-label={`${count} more activities`}\n      className=\"pointer-events-auto grid size-[54px] shrink-0 place-items-center rounded-full border border-activity-border bg-activity text-xs font-medium text-activity-muted-foreground shadow-lg shadow-black/[0.04] dark:shadow-black/30\"\n      initial={{ opacity: 0, scale: 0.8 }}\n      animate={{ opacity: 1, scale: 1 }}\n      exit={{ opacity: 0, scale: 0.8 }}\n    >\n      +{count}\n    </motion.button>\n  )\n}\n\nfunction StackLayout({ entries }: { entries: ActivityEntry[] }) {\n  return (\n    <AnimatePresence initial={false} mode=\"popLayout\">\n      {entries.map((entry) => (\n        <ViewportEntry key={entry.id} entry={entry} />\n      ))}\n    </AnimatePresence>\n  )\n}\n\nfunction SingleLayout({ entries }: { entries: ActivityEntry[] }) {\n  const focused = useFocused()\n  const current = entries.find((entry) => entry.id === focused)\n  return <AnimatePresence initial={false}>{current && <ViewportEntry key=\"single\" entry={current} />}</AnimatePresence>\n}\n\nfunction IslandLayout({ entries, anchor }: { entries: ActivityEntry[]; anchor: ActivityAnchor }) {\n  const focused = useFocused()\n  const primary = entries.find((entry) => entry.id === focused)\n  const others = entries.filter((entry) => entry.id !== focused).reverse()\n  const bubbles = others.slice(0, MAX_BUBBLES)\n  const overflow = others.slice(MAX_BUBBLES)\n  const wide = primary ? activityStore.isExpanded(primary) : false\n  const top = anchor.y === \"start\"\n  const reserve = BUBBLE_RESERVE[bubbles.length + (overflow.length > 0 ? 1 : 0)]\n\n  return (\n    <div\n      className={cn(\n        \"flex max-w-full gap-2\",\n        wide ? \"w-[var(--activity-width)] flex-wrap\" : \"w-max flex-nowrap\",\n        top ? \"items-start\" : \"items-end\",\n        JUSTIFY[anchor.x]\n      )}\n    >\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        {primary && (\n          <ViewportEntry\n            key={primary.id}\n            entry={primary}\n            className={cn(\"min-w-0\", wide ? (top ? \"order-first basis-full\" : \"order-last basis-full\") : reserve)}\n          />\n        )}\n        {bubbles.map((entry) => (\n          <ViewportEntry key={entry.id} entry={entry} presentation=\"bubble\" className=\"shrink-0\" />\n        ))}\n        {overflow.map((entry) => (\n          <ViewportEntry key={entry.id} entry={entry} presentation=\"hidden\" />\n        ))}\n        {overflow.length > 0 && (\n          <MoreBubble key=\"more\" count={overflow.length} onClick={() => activityStore.focus(overflow[0].id)} />\n        )}\n      </AnimatePresence>\n    </div>\n  )\n}\n\nexport type ActivityTone = \"default\" | \"inverted\"\n\nexport const activityTones: Record<ActivityTone, React.CSSProperties | undefined> = {\n  default: undefined,\n  inverted: {\n    \"--activity-background\": \"var(--foreground)\",\n    \"--activity-foreground\": \"var(--background)\",\n    \"--activity-muted\": \"color-mix(in oklch, var(--background) 14%, var(--foreground))\",\n    \"--activity-muted-foreground\": \"color-mix(in oklch, var(--background) 62%, var(--foreground))\",\n    \"--activity-border\": \"color-mix(in oklch, var(--background) 12%, var(--foreground))\",\n  } as React.CSSProperties,\n}\n\nexport type ActivityViewportProps = {\n  /** Where activities appear. They grow away from this edge. @default \"bottom-right\" */\n  position?: ActivityPosition\n  /**\n   * How several activities share the screen.\n   * - `stack`: one pill each.\n   * - `single`: one pill that morphs into whatever started last.\n   * - `island`: the focused one in full, the rest as bubbles beside it.\n   * @default \"stack\"\n   */\n  layout?: ActivityLayout\n  /** `inverted` flips the surface against the page: dark on light, light on dark. @default \"default\" */\n  tone?: ActivityTone\n  /** Extra classes, such as `absolute` to place it inside a container, or `--activity-*` token overrides. */\n  className?: string\n  style?: React.CSSProperties\n}\n\n/** Shows every activity started with `activity` or the helpers. Render exactly one, near the root of your app. */\nexport function ActivityViewport({\n  position = \"bottom-right\",\n  layout = \"stack\",\n  tone = \"default\",\n  className,\n  style,\n}: ActivityViewportProps) {\n  const entries = useEntries()\n  const anchor = ANCHORS[position]\n\n  React.useEffect(() => activityStore.mountViewport(), [])\n\n  return (\n    <ActivityAnchorContext.Provider value={anchor}>\n      <section\n        aria-label=\"Activity\"\n        data-slot=\"activity-viewport\"\n        className={cn(\n          \"pointer-events-none fixed z-50 flex max-w-[calc(100vw-2rem)] gap-2 [--activity-width:min(22rem,calc(100vw-2rem))]\",\n          layout === \"island\" ? \"w-auto\" : \"w-[var(--activity-width)]\",\n          POSITIONS[position],\n          className\n        )}\n        style={{ ...activityTones[tone], ...style }}\n      >\n        <MotionConfig reducedMotion=\"user\">\n          {layout === \"stack\" && <StackLayout entries={entries} />}\n          {layout === \"single\" && <SingleLayout entries={entries} />}\n          {layout === \"island\" && <IslandLayout entries={entries} anchor={anchor} />}\n        </MotionConfig>\n      </section>\n    </ActivityAnchorContext.Provider>\n  )\n}\n\nexport {\n  Activity,\n  ActivityAnnounce,\n  ActivityButton,\n  ActivityCollapse,\n  ActivityCompact,\n  ActivityDismiss,\n  ActivityExpanded,\n  ActivityGlyph,\n  ActivityProgress,\n  ActivityReveal,\n  ActivityShared,\n  ActivityTrigger,\n  useActivity,\n  useActivityState,\n} from \"@/registry/kinematic/ui/activity-surface\"\nexport type {\n  ActivityButtonProps,\n  ActivityPresentation,\n  ActivityProps,\n  ActivitySharedProps,\n} from \"@/registry/kinematic/ui/activity-surface\"\nexport { activity, activityStore, createActivityState } from \"@/registry/kinematic/lib/activity-store\"\nexport type {\n  ActivityAction,\n  ActivityEntry,\n  ActivityHandle,\n  ActivityItem,\n  ActivityItemInput,\n  ActivityOutcome,\n  ActivityRun,\n  ActivityShowOptions,\n  ActivityStartOptions,\n  ActivityState,\n  ActivityTask,\n  ActivityTrackOptions,\n  ActivityView,\n  ActivityViewHandle,\n} from \"@/registry/kinematic/lib/activity-store\"\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/kinematic/ui/activity-surface.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { animate, LayoutGroup, motion, MotionConfig, useMotionValue, type Transition } from \"motion/react\"\nimport { ChevronDown, X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useControllableState } from \"@/registry/kinematic/hooks/use-controllable-state\"\nimport type { ActivityState } from \"@/registry/kinematic/lib/activity-store\"\nimport { useEasePreset } from \"@/registry/kinematic/lib/motion\"\n\nconst COMPACT_RADIUS = 26\nconst EXPANDED_RADIUS = 22\nconst ENTER_OFFSET = 12\nconst EXIT_OFFSET = 8\n\nexport type ActivityPresentation = \"full\" | \"bubble\" | \"hidden\"\n\nexport type ActivityAnchor = { x: \"start\" | \"center\" | \"end\"; y: \"start\" | \"end\" }\n\ntype ActivityContextValue = {\n  id: string\n  presentation: ActivityPresentation\n  expanded: boolean\n  setExpanded: (expanded: boolean) => void\n  focus?: () => void\n  dismiss?: () => void\n  morph: Transition\n  fade: Transition\n}\n\nconst ActivityContext = React.createContext<ActivityContextValue | null>(null)\n\nexport const ActivityAnchorContext = React.createContext<ActivityAnchor>({ x: \"center\", y: \"start\" })\n\n/** The surrounding activity's state: `expanded`, `setExpanded`, `dismiss` and more. Use it to build custom parts. */\nexport function useActivity() {\n  const context = React.useContext(ActivityContext)\n  if (!context) {\n    throw new Error(\n      \"Activity parts (ActivityCompact, ActivityShared, ActivityButton…) must be rendered inside an activity: either a view passed to activity.show({ view }), or an inline <Activity>.\"\n    )\n  }\n  return context\n}\n\n/** Reads a store made with `createActivityState` and re-renders when it changes. */\nexport function useActivityState<T>(state: ActivityState<T>) {\n  return React.useSyncExternalStore(state.subscribe, state.get, state.get)\n}\n\nfunction useSurfaceSize(\n  surfaceRef: React.RefObject<HTMLDivElement | null>,\n  contentRef: React.RefObject<HTMLDivElement | null>,\n  transition: Transition\n) {\n  const width = useMotionValue(0)\n  const height = useMotionValue(0)\n  const measured = React.useRef(false)\n\n  React.useLayoutEffect(() => {\n    const surface = surfaceRef.current\n    const content = contentRef.current\n    if (!surface || !content) return\n\n    const measure = () => {\n      const target = {\n        width: content.offsetWidth + surface.offsetWidth - surface.clientWidth,\n        height: content.offsetHeight + surface.offsetHeight - surface.clientHeight,\n      }\n      if (!measured.current) {\n        width.jump(target.width)\n        height.jump(target.height)\n        measured.current = true\n        return\n      }\n      animate(width, target.width, transition)\n      animate(height, target.height, transition)\n    }\n\n    measure()\n    const observer = new ResizeObserver(measure)\n    observer.observe(content)\n    return () => observer.disconnect()\n  }, [surfaceRef, contentRef, width, height, transition])\n\n  return { width, height }\n}\n\nfunction useFocusWithin(surfaceRef: React.RefObject<HTMLDivElement | null>) {\n  const within = React.useRef(false)\n\n  React.useEffect(() => {\n    const track = (event: FocusEvent) => {\n      within.current = !!surfaceRef.current?.contains(event.target as Node)\n    }\n    document.addEventListener(\"focusin\", track)\n    return () => document.removeEventListener(\"focusin\", track)\n  }, [surfaceRef])\n\n  return within\n}\n\nfunction useFocusHandoff(surfaceRef: React.RefObject<HTMLDivElement | null>, expanded: boolean) {\n  const previous = React.useRef(expanded)\n  const focusWithin = useFocusWithin(surfaceRef)\n\n  React.useEffect(() => {\n    const surface = surfaceRef.current\n    if (!surface || previous.current === expanded) return\n    previous.current = expanded\n    if (!focusWithin.current) return\n\n    const selector = expanded ? \"[data-slot=activity-expanded] button\" : \"[data-slot=activity-trigger]\"\n    const target = surface.querySelector<HTMLElement>(selector) ?? surface.querySelector<HTMLElement>(\"button\")\n    target?.focus({ preventScroll: true })\n  }, [surfaceRef, expanded, focusWithin])\n}\n\nfunction useOutsidePress(\n  surfaceRef: React.RefObject<HTMLDivElement | null>,\n  enabled: boolean,\n  onOutsidePress: () => void\n) {\n  const handler = React.useRef(onOutsidePress)\n\n  React.useEffect(() => {\n    handler.current = onOutsidePress\n  })\n\n  React.useEffect(() => {\n    if (!enabled) return\n    const onPointerDown = (event: PointerEvent) => {\n      if (!surfaceRef.current?.contains(event.target as Node)) handler.current()\n    }\n    document.addEventListener(\"pointerdown\", onPointerDown)\n    return () => document.removeEventListener(\"pointerdown\", onPointerDown)\n  }, [surfaceRef, enabled])\n}\n\nexport type ActivityProps = {\n  /** Scopes shared element names. Give each inline activity on a page its own. @default a generated id */\n  id?: string\n  /** Controlled open state. Pair with `onExpandedChange`. */\n  expanded?: boolean\n  /** Uncontrolled initial open state. @default false */\n  defaultExpanded?: boolean\n  onExpandedChange?: (expanded: boolean) => void\n  /** Enables `ActivityDismiss` and is called when it's pressed. */\n  onDismiss?: () => void\n  /** Called when the activity's bubble is pressed in the `island` layout. */\n  onFocus?: () => void\n  /** `full` is the pill and card, `bubble` shows only `ActivityGlyph`, `hidden` keeps it running unseen. @default \"full\" */\n  presentation?: ActivityPresentation\n  /** Fold back into the pill when the user presses outside it. @default true */\n  closeOnOutsideClick?: boolean\n  /** Written to `data-phase` for styling, e.g. `data-[phase=done]:…`. */\n  phase?: string\n  className?: string\n  children: React.ReactNode\n}\n\n/**\n * One activity surface. The viewport renders these for you; render one yourself to place an activity inline,\n * outside the viewport.\n */\nexport function Activity({\n  id,\n  expanded,\n  defaultExpanded = false,\n  onExpandedChange,\n  onDismiss,\n  onFocus,\n  presentation = \"full\",\n  closeOnOutsideClick = true,\n  phase,\n  className,\n  children,\n}: ActivityProps) {\n  const fallbackId = React.useId()\n  const key = id ?? fallbackId\n  const [open, setOpen] = useControllableState({\n    value: expanded,\n    defaultValue: defaultExpanded,\n    onChange: onExpandedChange,\n  })\n  const morph = useEasePreset(\"morphIn\")\n  const fade = useEasePreset(\"base\")\n  const anchor = React.useContext(ActivityAnchorContext)\n  const surfaceRef = React.useRef<HTMLDivElement>(null)\n  const contentRef = React.useRef<HTMLDivElement>(null)\n  const size = useSurfaceSize(surfaceRef, contentRef, morph)\n  const isExpanded = open && presentation === \"full\"\n  useFocusHandoff(surfaceRef, isExpanded)\n  useOutsidePress(surfaceRef, isExpanded && closeOnOutsideClick, () => setOpen(false))\n\n  const context = React.useMemo<ActivityContextValue>(\n    () => ({\n      id: key,\n      presentation,\n      expanded: isExpanded,\n      setExpanded: setOpen,\n      focus: onFocus,\n      dismiss: onDismiss,\n      morph,\n      fade,\n    }),\n    [key, presentation, isExpanded, setOpen, onFocus, onDismiss, morph, fade]\n  )\n\n  if (presentation === \"hidden\") {\n    return (\n      <ActivityContext.Provider value={context}>\n        <MotionConfig reducedMotion=\"user\">\n          <div hidden>{children}</div>\n        </MotionConfig>\n      </ActivityContext.Provider>\n    )\n  }\n\n  const direction = anchor.y === \"end\" ? 1 : -1\n\n  return (\n    <ActivityContext.Provider value={context}>\n      <MotionConfig reducedMotion=\"user\">\n        <motion.div\n          ref={surfaceRef}\n          data-slot=\"activity\"\n          data-phase={phase}\n          data-expanded={isExpanded || undefined}\n          data-presentation={presentation}\n          onKeyDown={(event) => {\n            if (event.key === \"Escape\" && isExpanded) setOpen(false)\n          }}\n          // Content stays pinned to the anchored edges while the window around it grows or shrinks: those edges never\n          // move, so the content holds still and the moving edges reveal it. Centred anchors pin left instead, since\n          // both sides move there and centring would clip the leading element mid-morph.\n          className={cn(\n            \"pointer-events-auto flex max-w-full overflow-hidden border border-activity-border bg-activity text-activity-foreground shadow-lg shadow-black/[0.04] dark:shadow-black/30\",\n            anchor.x === \"end\" ? \"justify-end\" : \"justify-start\",\n            anchor.y === \"end\" ? \"items-end\" : \"items-start\",\n            className\n          )}\n          style={{ width: size.width, height: size.height }}\n          initial={{ opacity: 0, y: direction * ENTER_OFFSET, filter: \"blur(6px)\", borderRadius: COMPACT_RADIUS }}\n          animate={{\n            opacity: 1,\n            y: 0,\n            filter: \"blur(0px)\",\n            borderRadius: isExpanded ? EXPANDED_RADIUS : COMPACT_RADIUS,\n          }}\n          exit={{ opacity: 0, y: direction * EXIT_OFFSET, filter: \"blur(6px)\" }}\n          transition={morph}\n        >\n          <div\n            ref={contentRef}\n            data-slot=\"activity-content\"\n            className={cn(\n              \"shrink-0 p-2\",\n              isExpanded\n                ? \"w-[var(--activity-width,22rem)]\"\n                : \"w-max max-w-[min(var(--activity-width,22rem),calc(100vw-2rem-var(--activity-reserve,0px)))]\"\n            )}\n          >\n            <LayoutGroup id={key}>\n              <motion.div\n                key={key}\n                initial={{ opacity: 0, filter: \"blur(4px)\" }}\n                animate={{ opacity: 1, filter: \"blur(0px)\" }}\n                transition={fade}\n              >\n                {children}\n              </motion.div>\n            </LayoutGroup>\n          </div>\n        </motion.div>\n      </MotionConfig>\n    </ActivityContext.Provider>\n  )\n}\n\ntype SlotProps = { className?: string; children: React.ReactNode }\n\n/** The pill. Shown while the activity is closed. */\nexport function ActivityCompact({ className, children }: SlotProps) {\n  const { expanded, presentation } = useActivity()\n  if (expanded || presentation !== \"full\") return null\n  return (\n    <div data-slot=\"activity-compact\" className={cn(\"flex items-center gap-3\", className)}>\n      {children}\n    </div>\n  )\n}\n\n/** The card. Shown while the activity is open. */\nexport function ActivityExpanded({ className, children }: SlotProps) {\n  const { expanded } = useActivity()\n  if (!expanded) return null\n  return (\n    <div data-slot=\"activity-expanded\" className={cn(\"flex w-full flex-col gap-1\", className)}>\n      {children}\n    </div>\n  )\n}\n\n/** A short, visually hidden message for screen readers. Change it when something worth hearing happens. */\nexport function ActivityAnnounce({ children }: { children?: React.ReactNode }) {\n  return (\n    <span role=\"status\" data-slot=\"activity-announce\" className=\"sr-only\">\n      {children}\n    </span>\n  )\n}\n\n/** What the activity shrinks to as a bubble in the `island` layout. `label` names its button. */\nexport function ActivityGlyph({ label, children }: { label: string; children: React.ReactNode }) {\n  const { presentation, focus } = useActivity()\n  if (presentation !== \"bubble\") return null\n  return (\n    <button\n      type=\"button\"\n      aria-label={`Show ${label}`}\n      onClick={focus}\n      className=\"grid place-items-center rounded-full outline-none focus-visible:ring-3 focus-visible:ring-ring/50\"\n    >\n      {children}\n    </button>\n  )\n}\n\n/** A button that opens and closes the activity. Usually wraps the pill's icon and text. */\nexport function ActivityTrigger({ className, children }: SlotProps) {\n  const { expanded, setExpanded } = useActivity()\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"activity-trigger\"\n      aria-expanded={expanded}\n      onClick={() => setExpanded(!expanded)}\n      className={cn(\n        \"flex min-w-0 items-center gap-3 rounded-full text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50\",\n        className\n      )}\n    >\n      {children}\n    </button>\n  )\n}\n\nexport type ActivitySharedProps = {\n  /** Matches this element with its copy in the other shape. Unique within one activity. */\n  name: string\n  /** Move without resizing. Use it for text, so it isn't stretched mid-morph. @default false */\n  text?: boolean\n  /** Corner radius in px for this shape; it animates between the two copies. */\n  radius?: number\n  className?: string\n  style?: React.CSSProperties\n  children?: React.ReactNode\n}\n\n/**\n * An element that keeps its identity between the pill and the card. Give the two copies the same `name` and it\n * travels and resizes between them. Set `text` for text, so it moves without being stretched.\n */\nexport function ActivityShared({ name, text = false, radius, className, style, children }: ActivitySharedProps) {\n  const { id, morph } = useActivity()\n  return (\n    <motion.span\n      layoutId={`${id}:${name}`}\n      layout={text ? \"position\" : true}\n      transition={morph}\n      className={cn(\"block\", className)}\n      style={radius === undefined ? style : { ...style, borderRadius: radius }}\n    >\n      {children}\n    </motion.span>\n  )\n}\n\nconst REVEAL_DELAY = 0.08\n\n/** Content that fades in once the card has opened. `delay` is in seconds. */\nexport function ActivityReveal({ delay = 0, className, children }: SlotProps & { delay?: number }) {\n  const { fade } = useActivity()\n  return (\n    <motion.div\n      className={className}\n      initial={{ opacity: 0, filter: \"blur(3px)\" }}\n      animate={{ opacity: 1, filter: \"blur(0px)\" }}\n      transition={{ ...fade, delay: REVEAL_DELAY + delay }}\n    >\n      {children}\n    </motion.div>\n  )\n}\n\nconst BUTTON_VARIANTS = {\n  ghost: \"text-activity-muted-foreground hover:bg-activity-foreground/[0.06] hover:text-activity-foreground\",\n  soft: \"bg-activity-foreground/[0.06] hover:bg-activity-foreground/[0.1]\",\n  solid: \"bg-activity-foreground text-activity hover:opacity-90\",\n  danger: \"bg-red-600 text-white hover:opacity-90\",\n} as const\n\nconst BUTTON_SIZES = {\n  \"icon-sm\": \"size-8 [&_svg]:size-4\",\n  \"icon-md\": \"size-10 [&_svg]:size-5\",\n  \"icon-lg\": \"size-12 [&_svg]:size-5\",\n  sm: \"h-8 gap-1.5 px-3.5 text-xs [&_svg]:size-3.5\",\n  lg: \"h-12 gap-2 px-5 text-sm [&_svg]:size-4\",\n} as const\n\nexport type ActivityButtonProps = React.ComponentProps<\"button\"> & {\n  variant?: keyof typeof BUTTON_VARIANTS\n  size?: keyof typeof BUTTON_SIZES\n}\n\n/** A button styled to match the surface. Takes every `<button>` prop plus `variant` and `size`. */\nexport function ActivityButton({\n  variant = \"ghost\",\n  size = \"icon-sm\",\n  type = \"button\",\n  className,\n  ...props\n}: ActivityButtonProps) {\n  return (\n    <button\n      type={type}\n      className={cn(\n        \"inline-flex shrink-0 items-center justify-center rounded-full font-medium transition-[background-color,color,opacity] outline-none focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50\",\n        BUTTON_VARIANTS[variant],\n        BUTTON_SIZES[size],\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/** A ready-made button that folds the card back into the pill. */\nexport function ActivityCollapse() {\n  const { setExpanded } = useActivity()\n  return (\n    <ActivityButton aria-label=\"Collapse\" onClick={() => setExpanded(false)}>\n      <ChevronDown className=\"rotate-180\" />\n    </ActivityButton>\n  )\n}\n\n/** A ready-made button that removes the activity. Renders nothing unless the activity can be dismissed. */\nexport function ActivityDismiss() {\n  const { dismiss } = useActivity()\n  if (!dismiss) return null\n  return (\n    <ActivityButton aria-label=\"Dismiss\" onClick={dismiss}>\n      <X />\n    </ActivityButton>\n  )\n}\n\nconst MIN_PROGRESS = 0.02\n\n/** A small progress ring. `value` is from 0 to 1. */\nexport function ActivityProgress({ value, className }: { value: number; className?: string }) {\n  return (\n    <svg viewBox=\"0 0 20 20\" className={cn(\"size-5 -rotate-90\", className)} aria-hidden>\n      <circle cx=\"10\" cy=\"10\" r=\"8\" fill=\"none\" strokeWidth=\"1.25\" className=\"stroke-activity-foreground/10\" />\n      <motion.circle\n        cx=\"10\"\n        cy=\"10\"\n        r=\"8\"\n        fill=\"none\"\n        strokeWidth=\"1.25\"\n        strokeLinecap=\"round\"\n        className=\"stroke-activity-foreground\"\n        initial={false}\n        animate={{ pathLength: Math.max(MIN_PROGRESS, value) }}\n        transition={{ duration: 0.3, ease: \"easeOut\" }}\n      />\n    </svg>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/kinematic/ui/activity-task.tsx",
      "content": "\"use client\"\n\nimport { AlertCircle, Check, ChevronDown, FileText, RotateCw } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport type { ActivityItem, ActivityTask } from \"@/registry/kinematic/lib/activity-store\"\nimport {\n  ActivityAnnounce,\n  ActivityButton,\n  ActivityCollapse,\n  ActivityCompact,\n  ActivityDismiss,\n  ActivityExpanded,\n  ActivityGlyph,\n  ActivityProgress,\n  ActivityReveal,\n  ActivityShared,\n  ActivityTrigger,\n  useActivity,\n} from \"@/registry/kinematic/ui/activity-surface\"\n\nconst STACK_SIZE = 3\nconst TILE_RADIUS_ROUND = 18\nconst TILE_RADIUS_SQUARE = 10\nconst ROW_STAGGER = 0.03\n// Status hues pulled toward the foreground so small text stays readable on light, dark and inverted surfaces.\nconst ATTENTION_TEXT =\n  \"text-[color-mix(in_oklab,var(--color-amber-500),var(--activity-foreground,var(--popover-foreground))_40%)]\"\nconst ERROR_TEXT =\n  \"text-[color-mix(in_oklab,var(--color-red-500),var(--activity-foreground,var(--popover-foreground))_25%)]\"\n\nexport type TaskPhase = \"running\" | \"attention\" | \"done\"\n\nfunction tally(task: ActivityTask) {\n  const done = task.items.filter((item) => item.status === \"done\").length\n  const failed = task.items.filter((item) => item.status === \"failed\").length\n  return { done, failed, total: task.items.length }\n}\n\nexport function taskPhase(task: ActivityTask): TaskPhase {\n  if (task.outcome) return \"done\"\n  return tally(task).failed > 0 ? \"attention\" : \"running\"\n}\n\nfunction taskSummary(task: ActivityTask) {\n  const { done, failed, total } = tally(task)\n  const needs = `need${failed === 1 ? \"s\" : \"\"} attention`\n  if (failed === 0) return `${done} of ${total} done`\n  if (done === 0) return `${failed} ${needs}`\n  return `${done} ready · ${failed} ${needs}`\n}\n\nfunction taskAnnouncement(task: ActivityTask) {\n  if (task.outcome) return task.outcome.label\n  return tally(task).failed > 0 ? `${task.title}: ${taskSummary(task)}` : \"\"\n}\n\nfunction taskProgress(task: ActivityTask) {\n  if (task.items.length === 0) return 1\n  const sum = task.items.reduce((total, item) => total + (item.status === \"done\" ? 1 : item.progress), 0)\n  return sum / task.items.length\n}\n\nfunction Tile({ item, round, className }: { item: ActivityItem; round: boolean; className?: string }) {\n  return (\n    <ActivityShared\n      name={`${item.id}:tile`}\n      radius={round ? TILE_RADIUS_ROUND : TILE_RADIUS_SQUARE}\n      className={cn(\n        \"relative grid size-9 shrink-0 place-items-center bg-activity-muted text-activity-muted-foreground [&_svg]:size-4\",\n        className\n      )}\n    >\n      {item.icon ?? <FileText />}\n      {item.status === \"failed\" && (\n        <span className=\"absolute -top-0.5 -right-0.5 size-2.5 rounded-full bg-red-500 ring-2 ring-activity\" />\n      )}\n    </ActivityShared>\n  )\n}\n\nfunction TileStack({ task }: { task: ActivityTask }) {\n  return (\n    <span className=\"flex shrink-0\">\n      {task.items.slice(0, STACK_SIZE).map((item, index) => (\n        <Tile key={item.id} item={item} round className={cn(\"ring-2 ring-activity\", index > 0 && \"-ml-4\")} />\n      ))}\n    </span>\n  )\n}\n\nfunction TaskHeadline({ task }: { task: ActivityTask }) {\n  const phase = taskPhase(task)\n  return (\n    <span className=\"flex min-w-0 flex-col text-left\">\n      <ActivityShared name=\"title\" text className=\"truncate text-sm font-medium\">\n        {task.outcome?.label ?? task.title}\n      </ActivityShared>\n      {phase !== \"done\" && (\n        <ActivityShared\n          name=\"summary\"\n          text\n          className={cn(\n            \"truncate text-xs tabular-nums\",\n            phase === \"attention\" ? ATTENTION_TEXT : \"text-activity-muted-foreground\"\n          )}\n        >\n          <span className=\"flex items-center gap-1\">\n            {phase === \"attention\" && <AlertCircle className=\"size-3 shrink-0\" />}\n            {taskSummary(task)}\n          </span>\n        </ActivityShared>\n      )}\n    </span>\n  )\n}\n\nfunction OutcomeAction({ task }: { task: ActivityTask }) {\n  const action = task.outcome?.action\n  if (!action) return null\n  return (\n    <ActivityButton variant=\"solid\" size=\"sm\" onClick={action.onClick}>\n      {action.label}\n    </ActivityButton>\n  )\n}\n\nfunction TaskGlyph({ task }: { task: ActivityTask }) {\n  const [first] = task.items\n  return (\n    <ActivityGlyph label={task.outcome?.label ?? task.title}>\n      <span className=\"relative grid place-items-center\">\n        {first && <Tile item={first} round />}\n        {taskPhase(task) === \"running\" && (\n          <ActivityProgress value={taskProgress(task)} className=\"pointer-events-none absolute size-[46px]\" />\n        )}\n      </span>\n    </ActivityGlyph>\n  )\n}\n\nfunction TaskCompact({ task }: { task: ActivityTask }) {\n  const { setExpanded } = useActivity()\n  const phase = taskPhase(task)\n  return (\n    <ActivityCompact>\n      <ActivityTrigger>\n        <TileStack task={task} />\n        <TaskHeadline task={task} />\n      </ActivityTrigger>\n      {phase === \"running\" && <ActivityProgress value={taskProgress(task)} className=\"mr-2\" />}\n      {phase === \"attention\" && (\n        <ActivityButton aria-label=\"Show details\" onClick={() => setExpanded(true)}>\n          <ChevronDown />\n        </ActivityButton>\n      )}\n      {phase === \"done\" && (\n        <span className=\"flex items-center gap-1\">\n          <OutcomeAction task={task} />\n          <ActivityDismiss />\n        </span>\n      )}\n    </ActivityCompact>\n  )\n}\n\nfunction ItemStatus({ item }: { item: ActivityItem }) {\n  switch (item.status) {\n    case \"queued\":\n      return <span className=\"text-xs text-activity-muted-foreground\">Waiting</span>\n    case \"running\":\n      return <ActivityProgress value={item.progress} />\n    case \"done\":\n      return <Check className=\"size-4 text-activity-foreground\" aria-label=\"Done\" />\n    case \"failed\":\n      return (\n        <ActivityButton variant=\"soft\" size=\"sm\" onClick={item.retry} disabled={!item.retry}>\n          <RotateCw />\n          Retry\n        </ActivityButton>\n      )\n  }\n}\n\nfunction TaskRow({ item, index }: { item: ActivityItem; index: number }) {\n  const failed = item.status === \"failed\"\n  return (\n    <li className=\"flex items-center gap-3 rounded-[14px] p-1.5 pr-2\">\n      <Tile item={item} round={false} />\n      <ActivityReveal delay={index * ROW_STAGGER} className=\"flex min-w-0 flex-1 flex-col\">\n        <span className=\"truncate text-sm\">{item.label}</span>\n        <span className={cn(\"truncate text-xs\", failed ? ERROR_TEXT : \"text-activity-muted-foreground\")}>\n          {failed ? item.error : item.meta}\n        </span>\n      </ActivityReveal>\n      <ActivityReveal delay={(index + 1) * ROW_STAGGER} className=\"flex shrink-0 items-center\">\n        <ItemStatus item={item} />\n      </ActivityReveal>\n    </li>\n  )\n}\n\nfunction TaskExpanded({ task }: { task: ActivityTask }) {\n  const done = taskPhase(task) === \"done\"\n  return (\n    <ActivityExpanded>\n      <div className=\"flex items-center gap-2 py-1 pr-0.5 pl-2\">\n        <TaskHeadline task={task} />\n        <span className=\"ml-auto flex items-center\">\n          <ActivityCollapse />\n          <ActivityDismiss />\n        </span>\n      </div>\n      <ul className=\"flex flex-col\">\n        {task.items.map((item, index) => (\n          <TaskRow key={item.id} item={item} index={index} />\n        ))}\n      </ul>\n      {done && task.outcome?.action && (\n        <ActivityReveal className=\"flex justify-end p-1\">\n          <OutcomeAction task={task} />\n        </ActivityReveal>\n      )}\n    </ActivityExpanded>\n  )\n}\n\nexport function ActivityTaskView({ task }: { task: ActivityTask }) {\n  return (\n    <>\n      <ActivityAnnounce>{taskAnnouncement(task)}</ActivityAnnounce>\n      <TaskGlyph task={task} />\n      <TaskCompact task={task} />\n      <TaskExpanded task={task} />\n    </>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/kinematic/lib/activity-store.ts",
      "content": "import type * as React from \"react\"\n\nexport type ActivityItemStatus = \"queued\" | \"running\" | \"done\" | \"failed\"\n\nexport type ActivityItem = {\n  /** Unique within its task. Used to update the item. */\n  id: string\n  /** Main line of the row, e.g. a file name. */\n  label: string\n  /** Secondary line, e.g. a file size. Replaced by the error while the item has failed. */\n  meta?: string\n  /** Shown in the item's tile. Defaults to a file icon. */\n  icon?: React.ReactNode\n  status: ActivityItemStatus\n  /** From 0 to 1. */\n  progress: number\n  error?: string\n  /** Shows a Retry button while the item has failed. */\n  retry?: () => void\n}\n\nexport type ActivityItemInput = Pick<ActivityItem, \"id\" | \"label\" | \"meta\" | \"icon\">\n\nexport type ActivityAction = { label: string; onClick: () => void }\n\n/** What a finished task settles into: a label, and optionally one action button. */\nexport type ActivityOutcome = { label: string; action?: ActivityAction }\n\nexport type ActivityStage = \"working\" | \"settled\"\n\nexport type ActivityTask = {\n  kind: \"task\"\n  id: string\n  title: string\n  items: ActivityItem[]\n  outcome: ActivityOutcome | null\n  openedIn: ActivityStage | null\n}\n\nexport type ActivityView = {\n  kind: \"view\"\n  id: string\n  view: React.ComponentType\n  openedIn: ActivityStage | null\n  onDismiss?: () => void\n}\n\nexport type ActivityEntry = ActivityTask | ActivityView\n\n/**\n * Does one item's work. Call `report` with 0 to 1 as it goes; resolve when done, throw to fail the item.\n * `signal` aborts when the task is dismissed, so pass it to `fetch` or check it to stop early.\n */\nexport type ActivityRun = (report: (progress: number) => void, signal: AbortSignal) => Promise<unknown>\n\nexport type ActivityStartOptions = {\n  /** Shown while the task runs. */\n  title: string\n  /** Each item starts queued at 0. */\n  items: ActivityItemInput[]\n}\n\nexport type ActivityTrackOptions = {\n  /** Shown while the task runs. */\n  title: string\n  /** Each item's `run` does its work and reports progress. A failed item gets a Retry button. */\n  items: (ActivityItemInput & { run: ActivityRun })[]\n  /** Run items one after another. A failure pauses the rest until it's retried. @default false */\n  sequential?: boolean\n  /** What the task settles into once every item is done. */\n  outcome: (task: ActivityTask) => ActivityOutcome\n}\n\nexport type ActivityShowOptions = {\n  /** Reusing an id replaces that activity (its `onDismiss` runs first). @default a generated id */\n  id?: string\n  /** The activity's content, built from ActivityCompact, ActivityExpanded and the other parts. */\n  view: React.ComponentType\n  /** Runs when the activity is dismissed or replaced. Clean up here (stop audio, abort requests). */\n  onDismiss?: () => void\n}\n\nexport type ActivityViewHandle = {\n  id: string\n  /** Opens the card. */\n  expand: () => void\n  /** Folds it back into the pill. */\n  collapse: () => void\n  /** Removes it from the viewport. */\n  dismiss: () => void\n}\n\nexport type ActivityHandle = ActivityViewHandle & {\n  /** Changes any field of an item. */\n  update: (itemId: string, patch: Partial<ActivityItem>) => void\n  /** Marks the item running at `progress` (0 to 1, clamped). */\n  progress: (itemId: string, progress: number) => void\n  /** Marks the item done and clears any error. */\n  complete: (itemId: string) => void\n  /** Marks the item failed. Pass `retry` to show a Retry button. */\n  fail: (itemId: string, failure: { error: string; retry?: () => void }) => void\n  /** Settles the task into its outcome. */\n  finish: (outcome: ActivityOutcome) => void\n}\n\nconst FALLBACK_ERROR = \"Something went wrong\"\n\n// Typed locally so the file compiles without @types/node. Bundlers replace process.env.NODE_ENV at build time.\ndeclare const process: { env: { NODE_ENV?: string } } | undefined\nconst DEVELOPMENT = typeof process !== \"undefined\" && process.env.NODE_ENV === \"development\"\n\nlet entries: ActivityEntry[] = []\nlet focused: string | null = null\nlet counter = 0\nlet viewports = 0\nconst listeners = new Set<() => void>()\nconst warned = new Set<string>()\nconst aborters = new Map<string, AbortController>()\n\nfunction warnOnce(key: string, message: string) {\n  if (!DEVELOPMENT || warned.has(key)) return\n  warned.add(key)\n  console.warn(`[kinematic] ${message}`)\n}\n\nfunction mountViewport() {\n  viewports += 1\n  if (viewports > 1) {\n    warnOnce(\n      \"viewports\",\n      \"More than one <ActivityViewport /> is mounted, so every activity appears in each of them. Render a single viewport near the root of your app.\"\n    )\n  }\n  return () => {\n    viewports -= 1\n  }\n}\n\n// Checked a tick later, so a viewport mounting in the same render still counts.\nfunction expectViewport() {\n  if (!DEVELOPMENT) return\n  setTimeout(() => {\n    if (viewports > 0 || entries.length === 0) return\n    warnOnce(\n      \"no-viewport\",\n      \"An activity was started, but no <ActivityViewport /> is mounted, so nothing is shown. Render <ActivityViewport /> once near the root of your app, for example in app/layout.tsx.\"\n    )\n  })\n}\n\nfunction nextId() {\n  counter += 1\n  return `activity-${counter}`\n}\n\nfunction emit() {\n  listeners.forEach((listener) => listener())\n}\n\nfunction subscribe(listener: () => void) {\n  listeners.add(listener)\n  return () => {\n    listeners.delete(listener)\n  }\n}\n\nfunction getEntries() {\n  return entries\n}\n\nfunction isTask(entry: ActivityEntry): entry is ActivityTask {\n  return entry.kind === \"task\"\n}\n\nfunction getTasks() {\n  return entries.filter(isTask)\n}\n\nfunction findTask(id: string) {\n  return entries.find((entry): entry is ActivityTask => entry.id === id && isTask(entry))\n}\n\nfunction getFocused() {\n  const present = focused !== null && entries.some((entry) => entry.id === focused)\n  return present ? focused : (entries.at(-1)?.id ?? null)\n}\n\nfunction focus(id: string) {\n  focused = id\n  emit()\n}\n\nfunction updateEntry(id: string, change: (entry: ActivityEntry) => ActivityEntry) {\n  entries = entries.map((entry) => (entry.id === id ? change(entry) : entry))\n  emit()\n}\n\nfunction updateTask(id: string, change: (task: ActivityTask) => ActivityTask) {\n  updateEntry(id, (entry) => (isTask(entry) ? change(entry) : entry))\n}\n\nfunction updateItem(taskId: string, itemId: string, patch: Partial<ActivityItem>) {\n  updateTask(taskId, (task) => ({\n    ...task,\n    items: task.items.map((item) => (item.id === itemId ? { ...item, ...patch } : item)),\n  }))\n}\n\nfunction release(entry: ActivityEntry | undefined) {\n  if (entry?.kind === \"view\") entry.onDismiss?.()\n  if (entry?.kind === \"task\") {\n    aborters.get(entry.id)?.abort()\n    aborters.delete(entry.id)\n  }\n}\n\nfunction dismiss(id: string) {\n  release(entries.find((entry) => entry.id === id))\n  entries = entries.filter((entry) => entry.id !== id)\n  emit()\n}\n\nfunction clear() {\n  entries.forEach(release)\n  entries = []\n  emit()\n}\n\nfunction stageOf(entry: ActivityEntry): ActivityStage {\n  return isTask(entry) && entry.outcome ? \"settled\" : \"working\"\n}\n\nfunction isExpanded(entry: ActivityEntry) {\n  return entry.openedIn === stageOf(entry)\n}\n\nfunction setExpanded(id: string, expanded: boolean) {\n  updateEntry(id, (entry) => ({ ...entry, openedIn: expanded ? stageOf(entry) : null }))\n}\n\nfunction clamp(value: number) {\n  return Math.min(1, Math.max(0, value))\n}\n\nfunction createViewHandle(id: string): ActivityViewHandle {\n  return {\n    id,\n    expand: () => setExpanded(id, true),\n    collapse: () => setExpanded(id, false),\n    dismiss: () => dismiss(id),\n  }\n}\n\nfunction createTaskHandle(id: string): ActivityHandle {\n  return {\n    ...createViewHandle(id),\n    update: (itemId, patch) => updateItem(id, itemId, patch),\n    progress: (itemId, progress) => updateItem(id, itemId, { status: \"running\", progress: clamp(progress) }),\n    complete: (itemId) => updateItem(id, itemId, { status: \"done\", progress: 1, error: undefined, retry: undefined }),\n    fail: (itemId, failure) => updateItem(id, itemId, { status: \"failed\", ...failure }),\n    finish: (outcome) => updateTask(id, (task) => ({ ...task, outcome })),\n  }\n}\n\nfunction start({ title, items }: ActivityStartOptions): ActivityHandle {\n  const id = nextId()\n  const task: ActivityTask = {\n    kind: \"task\",\n    id,\n    title,\n    outcome: null,\n    openedIn: null,\n    items: items.map((item) => ({ ...item, status: \"queued\", progress: 0 })),\n  }\n  entries = [...entries, task]\n  focused = id\n  emit()\n  expectViewport()\n  return createTaskHandle(id)\n}\n\nfunction show({ id = nextId(), view, onDismiss }: ActivityShowOptions): ActivityViewHandle {\n  const entry: ActivityView = { kind: \"view\", id, view, openedIn: null, onDismiss }\n  const previous = entries.find((current) => current.id === id)\n  release(previous)\n  entries = previous ? entries.map((current) => (current.id === id ? entry : current)) : [...entries, entry]\n  focused = id\n  emit()\n  expectViewport()\n  return createViewHandle(id)\n}\n\nfunction errorMessage(error: unknown) {\n  return error instanceof Error ? error.message : FALLBACK_ERROR\n}\n\nfunction track({ title, items, sequential = false, outcome }: ActivityTrackOptions): ActivityHandle {\n  const handle = start({ title, items })\n  const controller = new AbortController()\n  aborters.set(handle.id, controller)\n  type Item = (typeof items)[number]\n\n  const settle = () => {\n    const task = findTask(handle.id)\n    if (task && !task.outcome && task.items.every((item) => item.status === \"done\")) handle.finish(outcome(task))\n  }\n\n  const run = async (item: Item): Promise<boolean> => {\n    if (controller.signal.aborted) return false\n    handle.progress(item.id, 0)\n    try {\n      await item.run((value) => handle.progress(item.id, value), controller.signal)\n      handle.complete(item.id)\n      return true\n    } catch (error) {\n      handle.fail(item.id, { error: errorMessage(error), retry: () => void retry(item) })\n      return false\n    }\n  }\n\n  const runFrom = async (index: number) => {\n    for (const item of items.slice(index)) {\n      if (!(await run(item))) return\n    }\n  }\n\n  const retry = async (item: Item) => {\n    const current = findTask(handle.id)?.items.find((entry) => entry.id === item.id)\n    if (current?.status !== \"failed\") return\n    if (!(await run(item))) return\n    if (sequential) await runFrom(items.indexOf(item) + 1)\n    settle()\n  }\n\n  const begin = async () => {\n    if (sequential) await runFrom(0)\n    else await Promise.all(items.map(run))\n    settle()\n  }\n\n  void begin()\n  return handle\n}\n\n/** A tiny store you update from anywhere (`set`) and read inside a view with `useActivityState`. */\nexport function createActivityState<T>(initial: T) {\n  let value = initial\n  const subscribers = new Set<() => void>()\n  return {\n    get: () => value,\n    set: (next: T | ((current: T) => T)) => {\n      value = typeof next === \"function\" ? (next as (current: T) => T)(value) : next\n      subscribers.forEach((subscriber) => subscriber())\n    },\n    subscribe: (subscriber: () => void) => {\n      subscribers.add(subscriber)\n      return () => {\n        subscribers.delete(subscriber)\n      }\n    },\n  }\n}\n\nexport type ActivityState<T> = ReturnType<typeof createActivityState<T>>\n\nexport const activityStore = {\n  subscribe,\n  mountViewport,\n  getEntries,\n  getTasks,\n  getFocused,\n  focus,\n  dismiss,\n  clear,\n  setExpanded,\n  isExpanded,\n}\n\n/**\n * Starts and controls activities from anywhere, even outside React.\n * Needs one `<ActivityViewport />` mounted to show them.\n */\nexport const activity = {\n  /** Shows a task whose items you update yourself, e.g. from websocket events. */\n  start,\n  /** Runs each item's `run`, tracks progress, adds Retry on failure and settles into `outcome`. */\n  track,\n  /** Shows a custom activity. Reusing an `id` replaces the existing one. */\n  show,\n  /** Removes one activity by id. */\n  dismiss,\n  /** Removes every activity. */\n  clear,\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/registry/kinematic/lib/activity-audio.ts",
      "content": "type AudioWindow = Window & { webkitAudioContext?: typeof AudioContext }\n\nexport function createAudioContext() {\n  if (typeof window === \"undefined\") return null\n  const Context = window.AudioContext ?? (window as AudioWindow).webkitAudioContext\n  return Context ? new Context() : null\n}\n",
      "type": "registry:lib"
    }
  ],
  "cssVars": {
    "theme": {
      "color-activity": "var(--activity-background, var(--popover))",
      "color-activity-foreground": "var(--activity-foreground, var(--popover-foreground))",
      "color-activity-muted": "var(--activity-muted, var(--muted))",
      "color-activity-muted-foreground": "var(--activity-muted-foreground, var(--muted-foreground))",
      "color-activity-border": "var(--activity-border, var(--border))"
    }
  },
  "type": "registry:ui"
}