{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "expanding-action",
  "type": "registry:ui",
  "title": "Expanding Action",
  "description": "A compact action that transforms in place into a short set of related choices with accessible focus management.",
  "files": [
    {
      "path": "registry/sonaui/expanding-action/expanding-action.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { ChevronLeft } from \"lucide-react\";\nimport { MotionConfig, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type CSSProperties,\n  type ReactNode,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport useMeasure from \"react-use-measure\";\n\nimport { cn } from \"@/lib/sona-utils\";\n\nexport interface ExpandingActionItem {\n  /** Stable value passed to `onValueSelect` when the item is chosen. */\n  value: string;\n  /** Content displayed inside the choice button. */\n  label: ReactNode;\n  /** Whether the choice is unavailable. @default false */\n  disabled?: boolean;\n}\n\nexport interface ExpandingActionProps {\n  /** Short choices revealed when the action expands. */\n  items: ExpandingActionItem[];\n  /** Content displayed inside the collapsed trigger. */\n  trigger: ReactNode;\n  /** Optional icon displayed before the trigger content. @default undefined */\n  triggerIcon?: ReactNode;\n  /** Controlled expanded state. @default undefined */\n  open?: boolean;\n  /** Initial expanded state for uncontrolled usage. @default false */\n  defaultOpen?: boolean;\n  /** Called whenever the expanded state changes. @default undefined */\n  onOpenChange?: (open: boolean) => void;\n  /** Called with the selected item value before the action collapses. @default undefined */\n  onValueSelect?: (value: string) => void;\n  /** Accessible label for the control that returns to the trigger. @default \"Back\" */\n  backLabel?: string;\n  /** Whether the trigger and choices are unavailable. @default false */\n  disabled?: boolean;\n  /** Additional classes for the rendered state container. @default undefined */\n  className?: string;\n  /** Additional classes for the collapsed trigger. @default undefined */\n  triggerClassName?: string;\n  /** Additional classes applied to every choice button. @default undefined */\n  optionClassName?: string;\n}\n\nconst widthSpring = {\n  type: \"spring\",\n  stiffness: 260,\n  damping: 48,\n  mass: 0.9,\n} as const;\n\nconst tokenStyle = {\n  \"--expanding-action-surface\": \"var(--background)\",\n  \"--expanding-action-foreground\": \"var(--foreground)\",\n  \"--expanding-action-muted\": \"var(--muted-foreground)\",\n  \"--expanding-action-hover\":\n    \"color-mix(in oklab, var(--accent) 55%, transparent)\",\n  \"--expanding-action-border\": \"var(--border)\",\n  \"--expanding-action-ring\": \"var(--ring)\",\n} as CSSProperties;\n\nexport default function ExpandingAction({\n  items,\n  trigger,\n  triggerIcon,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  onValueSelect,\n  backLabel = \"Back\",\n  disabled = false,\n  className,\n  triggerClassName,\n  optionClassName,\n}: ExpandingActionProps) {\n  const [contentRef, contentBounds] = useMeasure();\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [isSurfaceAnimating, setIsSurfaceAnimating] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n  const isOpen = open ?? internalOpen;\n  const previousIsOpen = useRef(isOpen);\n  const hasEnabledItem = items.some((item) => !item.disabled);\n  const widthTransition = shouldReduceMotion ? { duration: 0 } : widthSpring;\n\n  const setOpen = (nextOpen: boolean) => {\n    if (open === undefined) setInternalOpen(nextOpen);\n    onOpenChange?.(nextOpen);\n  };\n\n  useLayoutEffect(() => {\n    if (previousIsOpen.current === isOpen) return;\n\n    previousIsOpen.current = isOpen;\n    setIsSurfaceAnimating(true);\n  }, [isOpen]);\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <motion.div\n        initial={false}\n        animate={\n          contentBounds.width ? { width: contentBounds.width } : undefined\n        }\n        onAnimationStart={() => setIsSurfaceAnimating(true)}\n        onAnimationComplete={() => setIsSurfaceAnimating(false)}\n        className={cn(\n          \"relative inline-flex max-w-full items-center rounded-full\",\n          isSurfaceAnimating ? \"overflow-x-hidden\" : \"overflow-x-auto\",\n          className,\n        )}\n        style={tokenStyle}\n        transition={widthTransition}\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 rounded-full border border-(--expanding-action-border) bg-(--expanding-action-surface)/70 shadow-sm\"\n        />\n        <div\n          ref={contentRef}\n          className={cn(\"relative inline-flex shrink-0\", isOpen && \"p-1\")}\n        >\n          {!isOpen ? (\n            <button\n              type=\"button\"\n              disabled={disabled || !hasEnabledItem}\n              onClick={() => setOpen(true)}\n              className={cn(\n                \"relative flex h-12 cursor-pointer items-center gap-2 rounded-full px-5 text-sm font-medium text-(--expanding-action-foreground) active:scale-[0.97] disabled:pointer-events-none disabled:opacity-45\",\n                \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--expanding-action-ring)\",\n                triggerClassName,\n              )}\n            >\n              {triggerIcon ? (\n                <span\n                  aria-hidden=\"true\"\n                  className=\"grid size-4 shrink-0 place-items-center\"\n                >\n                  {triggerIcon}\n                </span>\n              ) : null}\n              <span className=\"whitespace-nowrap\">{trigger}</span>\n            </button>\n          ) : (\n            <div className=\"relative flex items-center gap-1\">\n              <button\n                type=\"button\"\n                disabled={disabled}\n                onClick={() => setOpen(false)}\n                aria-label={backLabel}\n                className={cn(\n                  \"grid size-10 shrink-0 cursor-pointer place-items-center rounded-full text-(--expanding-action-muted) transition-colors hover:bg-(--expanding-action-hover) hover:text-(--expanding-action-foreground) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--expanding-action-ring) active:scale-[0.97] disabled:pointer-events-none disabled:opacity-45\",\n                )}\n              >\n                <ChevronLeft\n                  aria-hidden=\"true\"\n                  className=\"size-4\"\n                  strokeWidth={1.75}\n                />\n              </button>\n              <span\n                aria-hidden=\"true\"\n                className=\"h-5 w-px shrink-0 bg-(--expanding-action-border)\"\n              />\n              {items.map((item) => (\n                <button\n                  key={item.value}\n                  type=\"button\"\n                  disabled={disabled || item.disabled}\n                  onClick={() => {\n                    onValueSelect?.(item.value);\n                    setOpen(false);\n                  }}\n                  className={cn(\n                    \"h-10 shrink-0 cursor-pointer whitespace-nowrap rounded-full px-3 text-sm text-(--expanding-action-foreground) transition-colors hover:bg-(--expanding-action-hover) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--expanding-action-ring) active:scale-[0.97] disabled:pointer-events-none disabled:opacity-45\",\n                    optionClassName,\n                  )}\n                >\n                  {item.label}\n                </button>\n              ))}\n            </div>\n          )}\n        </div>\n      </motion.div>\n    </MotionConfig>\n  );\n}\n",
      "target": "components/ui/expanding-action/expanding-action.tsx"
    }
  ],
  "dependencies": [
    "lucide-react",
    "motion",
    "react-use-measure"
  ],
  "registryDependencies": [
    "@sona-ui/sona-utils"
  ]
}