{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tool-approval","type":"registry:component","title":"Tool Approval","description":"A card for an agent action waiting on approval, with Approve and Deny that collapse into a single status pill once resolved.","author":"Ryan","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/tool-approval.tsx","type":"registry:component","target":"@components/motion/tool-approval.tsx","content":"\"use client\";\n// easeui.dev/components/agents/tool-approval\n\nimport { Check, Terminal, X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ToolApprovalStatus = \"pending\" | \"approved\" | \"denied\";\n\nexport interface ToolApprovalProps {\n  /** What the agent wants to do. */\n  title: ReactNode;\n  /** The exact command, file path, or other detail worth reading before approving. */\n  description?: ReactNode;\n  /** Default \"pending\". Acting on the request is up to the caller: set this once onApprove or onDeny fires. */\n  status?: ToolApprovalStatus;\n  onApprove?: () => void;\n  onDeny?: () => void;\n  approveLabel?: string;\n  denyLabel?: string;\n  className?: string;\n}\n\nconst RESOLVED_ICON: Record<\"approved\" | \"denied\", ReactNode> = {\n  approved: <Check aria-hidden=\"true\" className=\"h-3 w-3\" strokeWidth={3} />,\n  denied: <X aria-hidden=\"true\" className=\"h-3 w-3\" strokeWidth={3} />,\n};\n\nconst RESOLVED_LABEL: Record<\"approved\" | \"denied\", string> = {\n  approved: \"Approved\",\n  denied: \"Denied\",\n};\n\nconst RESOLVED_CLASS: Record<\"approved\" | \"denied\", string> = {\n  approved: \"bg-success/15 text-success\",\n  denied: \"bg-destructive/15 text-destructive\",\n};\n\n/**\n * A card for an agent action waiting on approval, with Approve and Deny.\n * Resolving it collapses the buttons into a single status pill.\n */\nexport function ToolApproval({\n  title,\n  description,\n  status = \"pending\",\n  onApprove,\n  onDeny,\n  approveLabel = \"Approve\",\n  denyLabel = \"Deny\",\n  className,\n}: ToolApprovalProps) {\n  const reduce = useReducedMotion();\n  const pending = status === \"pending\";\n\n  return (\n    <motion.div\n      layout={!reduce}\n      transition={{ duration: 0.2, ease: EASE_OUT }}\n      className={cn(\"flex flex-col gap-3 rounded-2xl bg-card p-4 shadow-[0_0_0_1px_var(--border)]\", className)}\n    >\n      <div className=\"flex items-start gap-3\">\n        <span className=\"mt-0.5 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground\">\n          <Terminal aria-hidden=\"true\" className=\"h-3.5 w-3.5\" />\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          <p className=\"text-sm font-medium text-foreground\">{title}</p>\n          {description ? (\n            <p className=\"mt-1 truncate font-mono text-xs text-muted-foreground\">{description}</p>\n          ) : null}\n        </div>\n      </div>\n\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        {pending ? (\n          <motion.div\n            key=\"actions\"\n            initial={reduce ? false : { opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={reduce ? undefined : { opacity: 0 }}\n            transition={{ duration: 0.15, ease: EASE_OUT }}\n            className=\"flex justify-end gap-2\"\n          >\n            <button\n              type=\"button\"\n              onClick={onDeny}\n              className=\"inline-flex h-8 touch-manipulation items-center rounded-full px-3.5 text-sm font-medium text-muted-foreground outline-none transition-colors duration-150 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-foreground/40\"\n            >\n              {denyLabel}\n            </button>\n            <button\n              type=\"button\"\n              onClick={onApprove}\n              className=\"inline-flex h-8 touch-manipulation items-center rounded-full bg-foreground px-3.5 text-sm font-medium text-background outline-none transition-colors duration-150 hover:bg-foreground/90 focus-visible:ring-2 focus-visible:ring-foreground/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n            >\n              {approveLabel}\n            </button>\n          </motion.div>\n        ) : (\n          <motion.div\n            key=\"resolved\"\n            initial={reduce ? false : { opacity: 0, y: -4 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ duration: 0.2, ease: EASE_OUT }}\n            className={cn(\n              \"inline-flex w-fit items-center gap-1.5 self-end rounded-full px-2.5 py-1 text-xs font-medium\",\n              RESOLVED_CLASS[status],\n            )}\n          >\n            {RESOLVED_ICON[status]}\n            {RESOLVED_LABEL[status]}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </motion.div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Micro-interactions run 100 to 150ms, standard UI\n// 150 to 250ms, and panels up to 300ms.\n// Easing curves mirror the CSS custom properties in globals.css.\n\n/** ease-out-quint. Fast start that settles quickly. Entrances, exits, feedback. */\nexport const EASE_OUT = [0.23, 1, 0.32, 1] as const;\n/** ease-in-out-cubic. Elements already on screen moving to a new spot. */\nexport const EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;\n/** Sheet and drawer glide. */\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.23, 1, 0.32, 1)\";\n\n// Springs are described by duration and bounce, which is easier to reason about\n// than stiffness and damping. Bounce stays at zero for product UI.\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  duration: 0.15,\n  bounce: 0,\n} as const;\n\n/** Content swaps, label and icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  duration: 0.2,\n  bounce: 0,\n} as const;\n\n/** Overlay panel entrances, modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  duration: 0.25,\n  bounce: 0,\n} as const;\n\n/** Shared-layout glides, pills and indicators moving between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  duration: 0.22,\n  bounce: 0,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt). */\nexport const SPRING_MOUSE = {\n  stiffness: 320,\n  damping: 26,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders). Critically damped `useSpring` config,\n * so the value follows the pointer closely and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}