{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"icon-morphs","title":"Icon Morphs","description":"Menu button whose bars morph into a close icon. Search terms: actions.","dependencies":[],"registryDependencies":["@siteplane/base","@siteplane/button"],"files":[{"path":"packages/ui/src/components/icon-morphs/icon-morphs.tsx","content":"\"use client\";\n\nimport { type ReactElement, useState } from \"react\";\n\nimport {\n  type SiteplaneControlSize,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport { cn } from \"../../system/utils/cn\";\nimport type { ButtonBaseProps } from \"../button\";\nimport { KitButton } from \"../button/button-internal\";\n\nimport \"./icon-morphs.css\";\n\nexport type MenuIconMorphProps = Omit<\n  ButtonBaseProps,\n  \"aria-expanded\" | \"children\" | \"size\" | \"variant\"\n> & {\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  open?: boolean;\n  size?: SiteplaneControlSize;\n};\n\nexport function MenuIconMorph({\n  \"aria-label\": ariaLabel,\n  className,\n  defaultOpen = false,\n  disabled,\n  onClick,\n  onOpenChange,\n  open,\n  size,\n  type = \"button\",\n  ...props\n}: MenuIconMorphProps): ReactElement {\n  const isControlled = open !== undefined;\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const currentOpen = isControlled ? open : internalOpen;\n  const resolvedSize = useResolvedControlSize(size);\n\n  return (\n    <span\n      data-size={resolvedSize}\n      className=\"sp-menu-icon-morph-root\"\n      data-slot=\"menu-icon-morph-root\"\n    >\n      <KitButton\n        {...props}\n        aria-expanded={currentOpen}\n        aria-label={\n          ariaLabel ??\n          (currentOpen ? \"Close navigation menu\" : \"Open navigation menu\")\n        }\n        className={cn(\"sp-menu-icon-morph\", className)}\n        data-state={currentOpen ? \"open\" : \"closed\"}\n        disabled={disabled}\n        onClick={(event) => {\n          onClick?.(event);\n\n          if (event.defaultPrevented || disabled) {\n            return;\n          }\n\n          const nextOpen = !currentOpen;\n\n          if (!isControlled) {\n            setInternalOpen(nextOpen);\n          }\n\n          onOpenChange?.(nextOpen);\n        }}\n        profile=\"morph-trigger\"\n        size={`icon-${resolvedSize}`}\n        type={type}\n        variant=\"text\"\n      >\n        <span aria-hidden=\"true\" className=\"sp-menu-icon-bars\">\n          <span className=\"sp-menu-icon-bar sp-menu-icon-bar-one\" />\n          <span className=\"sp-menu-icon-bar sp-menu-icon-bar-two\" />\n          <span className=\"sp-menu-icon-bar sp-menu-icon-bar-three\" />\n        </span>\n      </KitButton>\n    </span>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/icon-morphs/icon-morphs.tsx"},{"path":"packages/ui/src/components/icon-morphs/icon-morphs.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  .sp-menu-icon-morph-root {\n    display: inline-flex;\n  }\n\n  /* The bars sit two thirds of the way from their earlier 2 to 2.75 px toward\n   * the 4/3 px stroke lucide draws at the Button's 16 px icon size, which is\n   * about 1.56 to 1.81 px. Browsers snap each bar to whole device pixels, so\n   * those fractions drew the middle bar thicker than the outer ones. The bars\n   * therefore use the nearest half pixels, 1.5 px up to default and 2 px for\n   * lg and xl; with the half-pixel gaps, every bar and every step between\n   * bars is a whole device pixel on 2x screens, and all three bars draw\n   * equally thick. The open state moves the outer bars by gap plus bar\n   * height, so they still cross on the middle bar. */\n  .sp-menu-icon-morph-root .sp-menu-icon-bars {\n    --sp-menu-icon-gap: 0.28125rem;\n    --sp-menu-icon-bar-height: 0.09375rem;\n    --sp-menu-icon-width: 1.40625rem;\n    --sp-menu-icon-pos-y-bar-one: 0;\n    --sp-menu-icon-pos-y-bar-three: 0;\n    --sp-menu-icon-scale-bar: 1;\n    --sp-menu-icon-rotate-bar-one: 0deg;\n    --sp-menu-icon-rotate-bar-three: 0deg;\n  }\n\n  .sp-menu-icon-morph-root[data-size=\"sm\"] .sp-menu-icon-bars {\n    --sp-menu-icon-gap: 0.25rem;\n    --sp-menu-icon-bar-height: 0.09375rem;\n    --sp-menu-icon-width: 1.25rem;\n  }\n\n  .sp-menu-icon-morph-root[data-size=\"md\"] .sp-menu-icon-bars {\n    --sp-menu-icon-gap: 0.28125rem;\n    --sp-menu-icon-bar-height: 0.09375rem;\n    --sp-menu-icon-width: 1.40625rem;\n  }\n\n  .sp-menu-icon-morph-root[data-size=\"lg\"] .sp-menu-icon-bars {\n    --sp-menu-icon-gap: 0.3125rem;\n    --sp-menu-icon-bar-height: 0.125rem;\n    --sp-menu-icon-width: 1.5625rem;\n  }\n\n  .sp-menu-icon-morph-root[data-size=\"xl\"] .sp-menu-icon-bars {\n    --sp-menu-icon-gap: 0.34375rem;\n    --sp-menu-icon-bar-height: 0.125rem;\n    --sp-menu-icon-width: 1.71875rem;\n  }\n\n  @media (min-width: 640px) {\n    .sp-menu-icon-morph-root .sp-menu-icon-bars {\n      --sp-menu-icon-gap: 0.25rem;\n      --sp-menu-icon-bar-height: 0.09375rem;\n      --sp-menu-icon-width: 1.25rem;\n    }\n\n    .sp-menu-icon-morph-root[data-size=\"sm\"] .sp-menu-icon-bars {\n      --sp-menu-icon-gap: 0.21875rem;\n      --sp-menu-icon-bar-height: 0.09375rem;\n      --sp-menu-icon-width: 1.125rem;\n    }\n\n    .sp-menu-icon-morph-root[data-size=\"md\"] .sp-menu-icon-bars {\n      --sp-menu-icon-gap: 0.25rem;\n      --sp-menu-icon-bar-height: 0.09375rem;\n      --sp-menu-icon-width: 1.25rem;\n    }\n\n    .sp-menu-icon-morph-root[data-size=\"lg\"] .sp-menu-icon-bars {\n      --sp-menu-icon-gap: 0.3125rem;\n      --sp-menu-icon-bar-height: 0.125rem;\n      --sp-menu-icon-width: 1.5rem;\n    }\n\n    .sp-menu-icon-morph-root[data-size=\"xl\"] .sp-menu-icon-bars {\n      --sp-menu-icon-gap: 0.3125rem;\n      --sp-menu-icon-bar-height: 0.125rem;\n      --sp-menu-icon-width: 1.5625rem;\n    }\n  }\n\n  .sp-menu-icon-morph-root .sp-menu-icon-bars {\n    --sp-menu-icon-pitch: calc(\n      var(--sp-menu-icon-gap) + var(--sp-menu-icon-bar-height)\n    );\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    gap: calc(var(--sp-menu-icon-pitch) - var(--sp-menu-icon-bar-height));\n  }\n\n  /* The bars start a whole pixel apart, so the browser snaps all three to the\n   * same rows and paints them equally thick at every line weight; at a\n   * fractional distance the middle bar came out thicker or thinner than the\n   * others. */\n  @supports (top: round(1.5px, 1px)) {\n    .sp-menu-icon-morph-root .sp-menu-icon-bars {\n      --sp-menu-icon-pitch: round(\n        calc(var(--sp-menu-icon-gap) + var(--sp-menu-icon-bar-height)),\n        1px\n      );\n    }\n  }\n\n  .sp-menu-icon-morph-root .sp-menu-icon-bar {\n    position: relative;\n    display: block;\n    height: var(--sp-menu-icon-bar-height);\n    width: var(--sp-menu-icon-width);\n    border-radius: var(--radius-4xl);\n    background-color: currentColor;\n  }\n\n  /* The bars move on the navigation easing, the curve of the navigation\n   * motion (M1). */\n  .sp-menu-icon-morph-root .sp-menu-icon-bar-one {\n    top: var(--sp-menu-icon-pos-y-bar-one);\n    transform: rotate(var(--sp-menu-icon-rotate-bar-one));\n    transition:\n      top var(--motion-duration-base) var(--motion-ease-navigation)\n        var(--motion-duration-micro),\n      transform var(--motion-duration-micro) var(--motion-ease-navigation);\n  }\n\n  .sp-menu-icon-morph-root .sp-menu-icon-bar-two {\n    transform: scaleX(var(--sp-menu-icon-scale-bar));\n    transition: transform var(--motion-duration-overlay)\n      var(--motion-ease-navigation) var(--motion-duration-micro);\n  }\n\n  .sp-menu-icon-morph-root .sp-menu-icon-bar-three {\n    bottom: var(--sp-menu-icon-pos-y-bar-three);\n    transform: rotate(var(--sp-menu-icon-rotate-bar-three));\n    transition:\n      bottom var(--motion-duration-base) var(--motion-ease-navigation)\n        var(--motion-duration-micro),\n      transform var(--motion-duration-micro) var(--motion-ease-navigation);\n  }\n\n  .sp-menu-icon-morph-root\n    .sp-menu-icon-morph[data-state=\"open\"]\n    .sp-menu-icon-bars {\n    --sp-menu-icon-pos-y-bar-one: var(--sp-menu-icon-pitch);\n    --sp-menu-icon-pos-y-bar-three: var(--sp-menu-icon-pitch);\n    --sp-menu-icon-scale-bar: 0;\n    --sp-menu-icon-rotate-bar-one: 45deg;\n    --sp-menu-icon-rotate-bar-three: -45deg;\n  }\n\n  .sp-menu-icon-morph-root\n    .sp-menu-icon-morph[data-state=\"open\"]\n    .sp-menu-icon-bar-one {\n    transition:\n      top var(--motion-duration-base) var(--motion-ease-navigation),\n      transform var(--motion-duration-base) var(--motion-ease-navigation)\n        var(--motion-duration-micro);\n  }\n\n  .sp-menu-icon-morph-root\n    .sp-menu-icon-morph[data-state=\"open\"]\n    .sp-menu-icon-bar-three {\n    transition:\n      bottom var(--motion-duration-base) var(--motion-ease-navigation),\n      transform var(--motion-duration-base) var(--motion-ease-navigation)\n        var(--motion-duration-micro);\n  }\n\n  /* The open-state transitions are more specific, so each switch uses !important. */\n  html[data-motion-reduce=\"true\"] .sp-menu-icon-morph-root .sp-menu-icon-bar,\n  :where([data-sp-motion=\"reduced\"])\n    .sp-menu-icon-morph-root\n    .sp-menu-icon-bar {\n    transition-duration: 0ms !important;\n    transition-delay: 0ms !important;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .sp-menu-icon-morph-root .sp-menu-icon-bar {\n      transition-duration: 0ms !important;\n      transition-delay: 0ms !important;\n    }\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/icon-morphs/icon-morphs.css"},{"path":"packages/ui/src/components/icon-morphs/index.ts","content":"export * from \"./icon-morphs\";\n","type":"registry:ui","target":"~/src/siteplane/components/icon-morphs/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/icon-morphs.md","categories":["Animated"],"meta":{"avoidWhen":["Avoid for a toggle that does not open a menu or navigation; use Toggle."],"description":"Menu button whose bars morph into a close icon.","knownLimitations":[],"providerNotes":[],"releaseStatus":"stable","searchTerms":["icon-morphs","icon morphs","actions","icon","morphs"],"useWhen":["Use for a menu button whose bars morph into a close icon while the navigation it controls is open."]}}