{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-buttons","title":"Animated Buttons","description":"Sweep and fixed-layout expanding button family. Search terms: actions.","dependencies":["lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/button"],"files":[{"path":"packages/ui/src/components/animated-buttons/animated-buttons.tsx","content":"\"use client\";\n\nimport { LogOutIcon } from \"lucide-react\";\nimport type { CSSProperties, ReactElement, ReactNode } from \"react\";\n\nimport type { ButtonBaseProps } from \"../button\";\nimport { KitButton } from \"../button/button-internal\";\nimport {\n  type SiteplaneControlSize,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport { cn } from \"../../system/utils/cn\";\n\nimport \"./animated-buttons.css\";\n\ntype AnimatedButtonVariant = Extract<\n  NonNullable<ButtonBaseProps[\"variant\"]>,\n  \"default\" | \"destructive\" | \"inverse\" | \"outline\"\n>;\n\nexport type FillSweepButtonProps = Omit<ButtonBaseProps, \"variant\"> & {\n  variant?: AnimatedButtonVariant;\n};\n\nexport function FillSweepButton({\n  children,\n  className,\n  variant = \"default\",\n  ...props\n}: FillSweepButtonProps): ReactElement {\n  return (\n    <span\n      className=\"sp-fill-sweep-button-root\"\n      data-slot=\"fill-sweep-button-root\"\n    >\n      <KitButton\n        {...props}\n        className={cn(\"sp-fill-sweep-button\", className)}\n        profile=\"animated-fill\"\n        variant={variant === \"destructive\" ? \"outline-destructive\" : variant}\n      >\n        <span aria-hidden=\"true\" className=\"sp-fill-sweep-fill\" />\n        <span className=\"sp-fill-sweep-label\">{children}</span>\n      </KitButton>\n    </span>\n  );\n}\n\nexport type ExpandingArrowButtonProps = Omit<\n  ButtonBaseProps,\n  \"children\" | \"size\" | \"variant\"\n> & {\n  label?: ReactNode;\n  size?: SiteplaneControlSize;\n  /**\n   * `text` is the bare arrow and label. `default` sets them on the default\n   * Button's surface, with its fill, hairline rim, depth and states.\n   */\n  variant?: \"text\" | \"default\";\n};\n\n// `type` stays in `props`: Button sets type=\"button\" only when it renders a\n// native button, so a `render` link does not receive it.\nexport function ExpandingArrowButton({\n  className,\n  label = \"Learn more\",\n  size,\n  variant = \"text\",\n  ...props\n}: ExpandingArrowButtonProps): ReactElement {\n  const resolvedSize = useResolvedControlSize(size);\n\n  return (\n    <span\n      data-size={resolvedSize}\n      className=\"sp-expanding-arrow-button-root\"\n      data-slot=\"expanding-arrow-button-root\"\n    >\n      <KitButton\n        {...props}\n        className={cn(\"sp-expanding-arrow-button\", className)}\n        profile=\"animated-arrow\"\n        size={resolvedSize}\n        variant={variant}\n      >\n        <span aria-hidden=\"true\" className=\"sp-expanding-arrow-circle\">\n          <span className=\"sp-expanding-arrow-icon\" />\n        </span>\n        <span className=\"sp-expanding-arrow-label\">{label}</span>\n      </KitButton>\n    </span>\n  );\n}\n\nexport type ExpandingLogoutButtonProps = Omit<\n  ButtonBaseProps,\n  \"children\" | \"size\" | \"variant\"\n> & {\n  label?: string;\n  size?: SiteplaneControlSize;\n  variant?: AnimatedButtonVariant;\n};\n\nexport function ExpandingLogoutButton({\n  \"aria-label\": ariaLabel,\n  className,\n  label = \"Log out\",\n  size,\n  style,\n  variant = \"destructive\",\n  ...props\n}: ExpandingLogoutButtonProps): ReactElement {\n  const resolvedSize = useResolvedControlSize(size);\n\n  return (\n    <span\n      data-size={resolvedSize}\n      className=\"sp-expanding-logout-button-root\"\n      data-slot=\"expanding-logout-button-root\"\n    >\n      {/* The open width fits the label; animated-buttons.css turns its length\n          into the width. */}\n      <span\n        className=\"sp-expanding-logout-slot\"\n        style={\n          {\n            \"--sp-expanding-logout-label-length\": label.length,\n          } as CSSProperties\n        }\n      >\n        <KitButton\n          {...props}\n          aria-label={ariaLabel ?? label}\n          className={cn(\"sp-expanding-logout-button\", className)}\n          data-variant={\n            variant === \"destructive\" ? \"outline-destructive\" : variant\n          }\n          profile=\"animated-expand\"\n          size={resolvedSize}\n          style={style}\n          variant={variant === \"destructive\" ? \"outline-destructive\" : variant}\n        >\n          <span aria-hidden=\"true\" className=\"sp-expanding-logout-sign\">\n            <LogOutIcon aria-hidden=\"true\" focusable=\"false\" />\n          </span>\n          <span className=\"sp-expanding-logout-label\">{label}</span>\n        </KitButton>\n      </span>\n    </span>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/animated-buttons/animated-buttons.tsx"},{"path":"packages/ui/src/components/animated-buttons/animated-buttons.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* Fill Sweep */\n  .sp-fill-sweep-button-root {\n    --sp-fill-sweep-radius: var(--radius-lg);\n    --sp-fill-sweep-duration: var(--motion-duration-moderate);\n    --sp-fill-sweep-inset: 0px;\n    --sp-fill-sweep-hover-width: 100%;\n    display: inline-flex;\n  }\n\n  [data-radius=\"xl\"] .sp-fill-sweep-button-root {\n    --sp-fill-sweep-radius: var(--radius-4xl);\n  }\n\n  .sp-fill-sweep-button-root .sp-fill-sweep-fill {\n    position: absolute;\n    inset: var(--sp-fill-sweep-inset) auto var(--sp-fill-sweep-inset)\n      var(--sp-fill-sweep-inset);\n    height: auto;\n    width: 0;\n    /* The Button clips the sweep once; a second rounded edge leaks its dark base. */\n    border-radius: 0;\n    background-color: var(--sp-fill-sweep-layer);\n    z-index: 20;\n    transition-property: width;\n    transition-duration: var(--sp-fill-sweep-duration);\n    transition-timing-function: var(--motion-ease-soft-in-out);\n  }\n\n  .sp-fill-sweep-button-root .sp-fill-sweep-label {\n    position: relative;\n    z-index: 30;\n  }\n\n  .sp-fill-sweep-button-root\n    .sp-fill-sweep-button:not(:disabled, [aria-disabled=\"true\"]):hover\n    .sp-fill-sweep-fill {\n    width: var(--sp-fill-sweep-hover-width);\n  }\n\n  :is(html[data-motion-reduce=\"true\"], [data-sp-motion=\"reduced\"])\n    .sp-fill-sweep-button-root\n    .sp-fill-sweep-fill {\n    transition-duration: 0ms;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .sp-fill-sweep-button-root .sp-fill-sweep-fill {\n      transition-duration: 0ms;\n    }\n  }\n\n  /* Expanding Arrow. The circle is the height of the control size at every\n   * viewport, like a Button of that size (S1); the arrow scales with it. */\n  .sp-expanding-arrow-button-root {\n    --sp-expanding-arrow-circle-size: var(--sp-rw-control-md);\n    --sp-expanding-arrow-left: 0.4rem;\n    --sp-expanding-arrow-line-width: 0.75rem;\n    --sp-expanding-arrow-line-height: 0.125rem;\n    --sp-expanding-arrow-head-size: 0.4rem;\n    --sp-expanding-arrow-head-right: 0.0625rem;\n    --sp-expanding-arrow-hover-x: 0.625rem;\n    --sp-expanding-arrow-text-size: 0.875rem;\n    --sp-expanding-arrow-radius: var(--radius-lg);\n    display: inline-flex;\n  }\n\n  [data-radius=\"xl\"] .sp-expanding-arrow-button-root {\n    --sp-expanding-arrow-radius: var(--radius-4xl);\n  }\n\n  .sp-expanding-arrow-button-root[data-size=\"sm\"] {\n    --sp-expanding-arrow-circle-size: var(--sp-rw-control-sm);\n    --sp-expanding-arrow-left: 0.35rem;\n    --sp-expanding-arrow-line-width: 0.65625rem;\n    --sp-expanding-arrow-head-size: 0.35rem;\n    --sp-expanding-arrow-hover-x: 0.5625rem;\n    --sp-expanding-arrow-text-size: 0.8125rem;\n  }\n\n  .sp-expanding-arrow-button-root[data-size=\"md\"] {\n    --sp-expanding-arrow-circle-size: var(--sp-rw-control-md);\n    --sp-expanding-arrow-left: 0.4rem;\n    --sp-expanding-arrow-line-width: 0.75rem;\n    --sp-expanding-arrow-head-size: 0.4rem;\n    --sp-expanding-arrow-hover-x: 0.625rem;\n  }\n\n  .sp-expanding-arrow-button-root[data-size=\"lg\"] {\n    --sp-expanding-arrow-circle-size: var(--sp-rw-control-lg);\n    --sp-expanding-arrow-left: 0.45rem;\n    --sp-expanding-arrow-line-width: 0.84375rem;\n    --sp-expanding-arrow-head-size: 0.45rem;\n    --sp-expanding-arrow-hover-x: 0.75rem;\n  }\n\n  .sp-expanding-arrow-button-root[data-size=\"xl\"] {\n    --sp-expanding-arrow-circle-size: var(--sp-rw-control-xl);\n    --sp-expanding-arrow-left: 0.6rem;\n    --sp-expanding-arrow-line-width: 1.125rem;\n    --sp-expanding-arrow-head-size: 0.6rem;\n    --sp-expanding-arrow-hover-x: 1rem;\n    --sp-expanding-arrow-text-size: 1rem;\n  }\n\n  /* Over a surface the fill paints above the Button's finish and below its\n   * rim, the label above both (design-rework.css, animated-arrow). */\n  .sp-expanding-arrow-button-root .sp-expanding-arrow-circle {\n    position: absolute;\n    z-index: 20;\n    inset: 0 auto 0 0;\n    display: block;\n    margin: 0;\n    width: var(--sp-expanding-arrow-circle-size);\n    height: 100%;\n    overflow: hidden;\n    border-radius: var(--sp-expanding-arrow-radius);\n    background: var(--sp-rw-bg-inverse);\n    transition: width var(--motion-duration-slow) var(--motion-ease-soft-in-out);\n  }\n\n  /* On a surface the fill starts flush in its corner, so it takes the\n   * surface's radius and fits it at rest and filled. */\n  .sp-expanding-arrow-button-root\n    .sp-expanding-arrow-button:not([data-variant=\"text\"])\n    .sp-expanding-arrow-circle {\n    border-radius: inherit;\n  }\n\n  .sp-expanding-arrow-button-root .sp-expanding-arrow-icon {\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: var(--sp-expanding-arrow-left);\n    width: var(--sp-expanding-arrow-line-width);\n    height: var(--sp-expanding-arrow-line-height);\n    margin: auto;\n    border-radius: 999px;\n    background: transparent;\n    transition:\n      background-color var(--motion-duration-slow)\n        var(--motion-ease-soft-in-out),\n      transform var(--motion-duration-slow) var(--motion-ease-soft-in-out);\n  }\n\n  .sp-expanding-arrow-button-root .sp-expanding-arrow-icon::before {\n    position: absolute;\n    content: \"\";\n    top: 50%;\n    right: var(--sp-expanding-arrow-head-right);\n    width: var(--sp-expanding-arrow-head-size);\n    height: var(--sp-expanding-arrow-head-size);\n    border-top: var(--sp-expanding-arrow-line-height) solid\n      var(--sp-rw-fg-inverse);\n    border-right: var(--sp-expanding-arrow-line-height) solid\n      var(--sp-rw-fg-inverse);\n    border-radius: 0 var(--sp-expanding-arrow-line-height) 0 0;\n    transform: translateY(-50%) rotate(45deg);\n  }\n\n  .sp-expanding-arrow-button-root .sp-expanding-arrow-label {\n    position: relative;\n    z-index: 30;\n    display: flex;\n    align-items: center;\n    min-height: var(--sp-expanding-arrow-circle-size);\n    padding: 0\n      calc(var(--sp-expanding-arrow-left) + var(--sp-expanding-arrow-hover-x)) 0\n      calc(var(--sp-expanding-arrow-circle-size) + 0.5rem);\n    color: var(--sp-rw-fg-primary);\n    font-size: var(--sp-expanding-arrow-text-size);\n    font-weight: var(--sp-rw-font-weight-default);\n    line-height: 1.6;\n    text-align: center;\n    transition: color var(--motion-duration-slow) var(--motion-ease-soft-in-out);\n  }\n\n  .sp-expanding-arrow-button-root\n    .sp-expanding-arrow-button:not(:disabled, [aria-disabled=\"true\"]):hover\n    .sp-expanding-arrow-circle {\n    width: 100%;\n  }\n\n  .sp-expanding-arrow-button-root\n    .sp-expanding-arrow-button:not(:disabled, [aria-disabled=\"true\"]):hover\n    .sp-expanding-arrow-icon {\n    background: var(--sp-rw-fg-inverse);\n    transform: translateX(var(--sp-expanding-arrow-hover-x));\n  }\n\n  .sp-expanding-arrow-button-root\n    .sp-expanding-arrow-button:not(:disabled, [aria-disabled=\"true\"]):hover\n    .sp-expanding-arrow-label {\n    color: var(--sp-rw-fg-inverse);\n  }\n\n  :is(html[data-motion-reduce=\"true\"], [data-sp-motion=\"reduced\"])\n    .sp-expanding-arrow-button-root\n    .sp-expanding-arrow-circle,\n  :is(html[data-motion-reduce=\"true\"], [data-sp-motion=\"reduced\"])\n    .sp-expanding-arrow-button-root\n    .sp-expanding-arrow-icon,\n  :is(html[data-motion-reduce=\"true\"], [data-sp-motion=\"reduced\"])\n    .sp-expanding-arrow-button-root\n    .sp-expanding-arrow-label {\n    transition-duration: 0ms;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .sp-expanding-arrow-button-root .sp-expanding-arrow-circle,\n    .sp-expanding-arrow-button-root .sp-expanding-arrow-icon,\n    .sp-expanding-arrow-button-root .sp-expanding-arrow-label {\n      transition-duration: 0ms;\n    }\n  }\n\n  /* Expanding Logout. The closed button is the square of the control size at\n   * every viewport, like a Button of that size (S1); the icon and the open\n   * width scale with it. */\n  .sp-expanding-logout-button-root {\n    --sp-expanding-logout-closed-size: var(--sp-rw-control-md);\n    --sp-expanding-logout-min-open-width: 6rem;\n    --sp-expanding-logout-icon-size: 0.875rem;\n    --sp-expanding-logout-duration: var(--motion-duration-slow);\n    --sp-expanding-logout-open-buffer: 1rem;\n    --sp-expanding-logout-sign-hover-width: 30%;\n    --sp-expanding-logout-sign-padding-left: 0.75rem;\n    --sp-expanding-logout-text-hover-width: 70%;\n    --sp-expanding-logout-text-padding-right: 0.375rem;\n    --sp-expanding-logout-text-size: 0.875rem;\n    --sp-expanding-logout-radius: var(--radius-lg);\n    display: inline-flex;\n  }\n\n  [data-radius=\"xl\"] .sp-expanding-logout-button-root {\n    --sp-expanding-logout-radius: var(--radius-4xl);\n  }\n\n  .sp-expanding-logout-button-root[data-size=\"sm\"] {\n    --sp-expanding-logout-closed-size: var(--sp-rw-control-sm);\n    --sp-expanding-logout-min-open-width: 5.5rem;\n    --sp-expanding-logout-icon-size: 0.75rem;\n    --sp-expanding-logout-open-buffer: 0.875rem;\n    --sp-expanding-logout-sign-padding-left: 0.625rem;\n    --sp-expanding-logout-text-padding-right: 0.3125rem;\n    --sp-expanding-logout-text-size: 0.8125rem;\n  }\n\n  .sp-expanding-logout-button-root[data-size=\"md\"] {\n    --sp-expanding-logout-closed-size: var(--sp-rw-control-md);\n    --sp-expanding-logout-min-open-width: 6rem;\n    --sp-expanding-logout-icon-size: 0.875rem;\n    --sp-expanding-logout-open-buffer: 1rem;\n    --sp-expanding-logout-sign-padding-left: 0.75rem;\n    --sp-expanding-logout-text-padding-right: 0.375rem;\n  }\n\n  .sp-expanding-logout-button-root[data-size=\"lg\"] {\n    --sp-expanding-logout-closed-size: var(--sp-rw-control-lg);\n    --sp-expanding-logout-min-open-width: 6.625rem;\n    --sp-expanding-logout-icon-size: 0.9375rem;\n    --sp-expanding-logout-open-buffer: 1.25rem;\n    --sp-expanding-logout-sign-padding-left: 0.875rem;\n    --sp-expanding-logout-text-padding-right: 0.4375rem;\n  }\n\n  .sp-expanding-logout-button-root[data-size=\"xl\"] {\n    --sp-expanding-logout-closed-size: var(--sp-rw-control-xl);\n    --sp-expanding-logout-min-open-width: 8.5rem;\n    --sp-expanding-logout-icon-size: 1.125rem;\n    --sp-expanding-logout-open-buffer: 2rem;\n    --sp-expanding-logout-sign-padding-left: 1.25rem;\n    --sp-expanding-logout-text-padding-right: 0.625rem;\n    --sp-expanding-logout-text-size: 1rem;\n  }\n\n  /* The open width fits the label: its length in characters, which the button\n   * sets as --sp-expanding-logout-label-length, beside the closed\n   * square and a buffer, and never less than the minimum. Where a Content\n   * Security Policy blocks that style attribute, the width fits the 7\n   * characters of the default \"Log out\". */\n  .sp-expanding-logout-button-root .sp-expanding-logout-slot {\n    --sp-expanding-logout-open-width: max(\n      var(--sp-expanding-logout-min-open-width),\n      calc(\n        var(--sp-expanding-logout-label-length, 7) * 1ch +\n          var(--sp-expanding-logout-closed-size) +\n          var(--sp-expanding-logout-open-buffer)\n      )\n    );\n    display: flex;\n    width: var(--sp-expanding-logout-open-width);\n    justify-content: center;\n  }\n\n  .sp-expanding-logout-button-root .sp-expanding-logout-sign {\n    width: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: currentColor;\n    transition-property: width, padding-left;\n    transition-duration: var(--sp-expanding-logout-duration);\n    transition-timing-function: var(--motion-ease-soft-in-out);\n  }\n\n  .sp-expanding-logout-button-root .sp-expanding-logout-sign svg {\n    width: var(--sp-expanding-logout-icon-size);\n    height: var(--sp-expanding-logout-icon-size);\n  }\n\n  .sp-expanding-logout-button-root .sp-expanding-logout-label {\n    position: absolute;\n    right: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 0;\n    opacity: 0;\n    color: currentColor;\n    font-size: var(--sp-expanding-logout-text-size);\n    font-weight: 600;\n    transition-property: opacity, width, padding-right;\n    transition-duration: var(--sp-expanding-logout-duration);\n    transition-timing-function: var(--motion-ease-soft-in-out);\n  }\n\n  .sp-expanding-logout-button-root\n    .sp-expanding-logout-button:not(:disabled, [aria-disabled=\"true\"]):is(\n      :hover,\n      :focus-visible\n    )\n    .sp-expanding-logout-sign {\n    width: var(--sp-expanding-logout-sign-hover-width);\n    padding-left: var(--sp-expanding-logout-sign-padding-left);\n  }\n\n  .sp-expanding-logout-button-root\n    .sp-expanding-logout-button:not(:disabled, [aria-disabled=\"true\"]):is(\n      :hover,\n      :focus-visible\n    )\n    .sp-expanding-logout-label {\n    opacity: 1;\n    width: var(--sp-expanding-logout-text-hover-width);\n    padding-right: var(--sp-expanding-logout-text-padding-right);\n  }\n\n  :is(html[data-motion-reduce=\"true\"], [data-sp-motion=\"reduced\"])\n    .sp-expanding-logout-button-root\n    .sp-expanding-logout-sign,\n  :is(html[data-motion-reduce=\"true\"], [data-sp-motion=\"reduced\"])\n    .sp-expanding-logout-button-root\n    .sp-expanding-logout-label {\n    transition-duration: 0ms;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .sp-expanding-logout-button-root .sp-expanding-logout-sign,\n    .sp-expanding-logout-button-root .sp-expanding-logout-label {\n      transition-duration: 0ms;\n    }\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/animated-buttons/animated-buttons.css"},{"path":"packages/ui/src/components/animated-buttons/index.ts","content":"export * from \"./animated-buttons\";\n","type":"registry:ui","target":"~/src/siteplane/components/animated-buttons/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/animated-buttons.md","categories":["Animated"],"meta":{"avoidWhen":["Avoid when a standard Button covers the need; Animated Buttons add hover motion only.","Avoid in narrow parents that would clip an expanding label."],"description":"Sweep and fixed-layout expanding button family.","knownLimitations":["An expanding action moves its content on hover; leave room for the expanded label."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["animated-buttons","animated buttons","actions","buttons"],"useWhen":["Use for an action that gains from hover motion. The family has a fill sweep button, an expanding arrow button and an expanding log out button."]}}