{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Human-in-the-loop approval card where the chosen option expands to fill the card while the alternatives collapse away.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Check } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst CHOICE_STAGGER = 0.03;\n\nexport type AIApprovalOption = {\n  /** Marks the option as the destructive one, e.g. \"Delete everything\". */\n  destructive?: boolean;\n  /** Secondary line under the label. */\n  detail?: string;\n  id: string;\n  label: string;\n};\n\nexport type AIApprovalProps = {\n  className?: string;\n  /** Extra context under the question. */\n  children?: ReactNode;\n  /** Called once, with the chosen option. */\n  onDecide?: (option: AIApprovalOption) => void;\n  options: AIApprovalOption[];\n  /** What the agent needs a human to settle. */\n  question: string;\n  /** Render already-resolved, e.g. when replaying a transcript. */\n  resolvedId?: string;\n};\n\n/**\n * The card an agent puts up before it acts.\n *\n * Choosing does not tick a radio button — the chosen option expands to fill the\n * card and the alternatives collapse out of existence. The decision should look\n * as irreversible as it is; leaving the rejected options sitting there greyed out\n * invites a second look at something that already happened.\n */\nconst AIApproval = ({\n  className,\n  children,\n  onDecide,\n  options,\n  question,\n  resolvedId,\n}: AIApprovalProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [chosenId, setChosenId] = useState<string | null>(resolvedId ?? null);\n\n  const chosen = options.find((option) => option.id === chosenId) ?? null;\n\n  const decide = (option: AIApprovalOption) => {\n    if (chosenId) {\n      return;\n    }\n    setChosenId(option.id);\n    onDecide?.(option);\n  };\n\n  return (\n    <motion.div\n      className={cn(\n        \"w-full rounded-xl border border-border bg-background p-3.5\",\n        className\n      )}\n      layout={!shouldReduceMotion}\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n    >\n      <motion.div layout={!shouldReduceMotion}>\n        <p className=\"font-medium text-foreground text-sm\">{question}</p>\n        {children ? (\n          <div className=\"mt-1 text-muted-foreground text-xs leading-relaxed\">\n            {children}\n          </div>\n        ) : null}\n      </motion.div>\n\n      <div className=\"mt-3\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {chosen ? (\n            <motion.div\n              animate={{ opacity: 1, scale: 1 }}\n              className={cn(\n                \"flex items-center gap-2 rounded-lg px-3 py-2 text-sm\",\n                chosen.destructive\n                  ? \"bg-destructive/10 text-destructive\"\n                  : \"bg-foreground text-background\"\n              )}\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1, scale: 1 }\n                  : { opacity: 0, scale: 0.97 }\n              }\n              key=\"resolved\"\n              transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n            >\n              <span className=\"flex size-4 items-center justify-center\">\n                <Check aria-hidden=\"true\" size={14} />\n              </span>\n              <span className=\"font-medium\">{chosen.label}</span>\n              {chosen.detail ? (\n                <span className=\"ml-auto text-xs opacity-70\">\n                  {chosen.detail}\n                </span>\n              ) : null}\n            </motion.div>\n          ) : (\n            <motion.div\n              className=\"flex flex-col gap-1.5\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, scale: 0.98 }\n              }\n              key=\"options\"\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.16, ease: EASE_OUT }\n              }\n            >\n              {options.map((option, index) => (\n                <motion.button\n                  animate={{ opacity: 1, y: 0 }}\n                  className={cn(\n                    \"flex w-full cursor-pointer items-center gap-2 rounded-lg border px-3 py-2 text-left text-sm transition-colors\",\n                    option.destructive\n                      ? \"border-destructive/30 text-destructive hover:bg-destructive/10\"\n                      : \"border-border text-foreground hover:bg-muted\"\n                  )}\n                  initial={\n                    shouldReduceMotion\n                      ? { opacity: 1, y: 0 }\n                      : { opacity: 0, y: 4 }\n                  }\n                  key={option.id}\n                  onClick={() => decide(option)}\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : { ...SPRING_DEFAULT, delay: index * CHOICE_STAGGER }\n                  }\n                  type=\"button\"\n                  whileTap={shouldReduceMotion ? undefined : { scale: 0.99 }}\n                >\n                  <span className=\"font-medium\">{option.label}</span>\n                  {option.detail ? (\n                    <span className=\"ml-auto text-muted-foreground text-xs\">\n                      {option.detail}\n                    </span>\n                  ) : null}\n                </motion.button>\n              ))}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </motion.div>\n  );\n};\n\nexport default AIApproval;\n","path":"index.tsx","target":"components/smoothui/ai-approval/index.tsx","type":"registry:ui"}],"name":"ai-approval","registryDependencies":[],"title":"Ai Approval","type":"registry:ui"}